Implement stream-based ZipFile ExtractToDirectory and CreateFromDirectory method...
authorCarlos Sánchez López <1175054+carlossanlop@users.noreply.github.com>
Tue, 30 May 2023 16:48:39 +0000 (09:48 -0700)
committerGitHub <noreply@github.com>
Tue, 30 May 2023 16:48:39 +0000 (09:48 -0700)
* ref: Add stream-based ZipFile.CreateFromDirectory and ZipFile.ExtractToDirectory methods.

* src: Add stream-based ZipFile.CreateFromDirectory ZipFile.ExtractToDirectory methods.

* tests: Move wrongly placed tests to the correct class.

* tests: Add stream-based tests for ZipFile.CreateFromDirectory and ZipFile.ExtractToDirectory.

* src: Documentation and resource strings.

* tests: More tests for unseekable/unreadable/unwritable streams.

* Address suggestions and include more exception validation tests.

* Fix braces after resolving conflict.

* Use AssertExtensions.SequenceEqual for clearer error message.

* Reset the resx file again.

* Order results of dir enumeration tests

12 files changed:
src/libraries/Common/tests/System/IO/Compression/ZipTestHelper.cs
src/libraries/System.IO.Compression.ZipFile/ref/System.IO.Compression.ZipFile.cs
src/libraries/System.IO.Compression.ZipFile/src/Resources/Strings.resx
src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Create.cs
src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Extract.cs
src/libraries/System.IO.Compression.ZipFile/tests/System.IO.Compression.ZipFile.Tests.csproj
src/libraries/System.IO.Compression.ZipFile/tests/ZipArchiveEntry.ExtractToDirectory.cs
src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Create.Stream.cs [new file with mode: 0644]
src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Create.cs
src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Extract.Stream.cs [new file with mode: 0644]
src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Extract.cs
src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Open.cs [new file with mode: 0644]

index f4a7d0c..6d19478 100644 (file)
@@ -329,9 +329,9 @@ namespace System.IO.Compression.Tests
 
         public static void DirFileNamesEqual(string actual, string expected)
         {
-            IEnumerable<string> actualEntries = Directory.EnumerateFileSystemEntries(actual, "*", SearchOption.AllDirectories);
-            IEnumerable<string> expectedEntries = Directory.EnumerateFileSystemEntries(expected, "*", SearchOption.AllDirectories);
-            Assert.True(Enumerable.SequenceEqual(expectedEntries.Select(i => Path.GetFileName(i)), actualEntries.Select(i => Path.GetFileName(i))));
+            IOrderedEnumerable<string> actualEntries = Directory.EnumerateFileSystemEntries(actual, "*", SearchOption.AllDirectories).Order();
+            IOrderedEnumerable<string> expectedEntries = Directory.EnumerateFileSystemEntries(expected, "*", SearchOption.AllDirectories).Order();
+            AssertExtensions.SequenceEqual(expectedEntries.Select(Path.GetFileName).ToArray(), actualEntries.Select(Path.GetFileName).ToArray());
         }
 
         private static void ItemEqual(string[] actualList, List<FileData> expectedList, bool isFile)
index 1637e94..bc0e207 100644 (file)
@@ -8,9 +8,16 @@ namespace System.IO.Compression
 {
     public static partial class ZipFile
     {
+        public static void CreateFromDirectory(string sourceDirectoryName, System.IO.Stream destination) { }
+        public static void CreateFromDirectory(string sourceDirectoryName, System.IO.Stream destination, System.IO.Compression.CompressionLevel compressionLevel, bool includeBaseDirectory) { }
+        public static void CreateFromDirectory(string sourceDirectoryName, System.IO.Stream destination, System.IO.Compression.CompressionLevel compressionLevel, bool includeBaseDirectory, System.Text.Encoding? entryNameEncoding) { }
         public static void CreateFromDirectory(string sourceDirectoryName, string destinationArchiveFileName) { }
         public static void CreateFromDirectory(string sourceDirectoryName, string destinationArchiveFileName, System.IO.Compression.CompressionLevel compressionLevel, bool includeBaseDirectory) { }
         public static void CreateFromDirectory(string sourceDirectoryName, string destinationArchiveFileName, System.IO.Compression.CompressionLevel compressionLevel, bool includeBaseDirectory, System.Text.Encoding? entryNameEncoding) { }
+        public static void ExtractToDirectory(System.IO.Stream source, string destinationDirectoryName) { }
+        public static void ExtractToDirectory(System.IO.Stream source, string destinationDirectoryName, bool overwriteFiles) { }
+        public static void ExtractToDirectory(System.IO.Stream source, string destinationDirectoryName, System.Text.Encoding? entryNameEncoding) { }
+        public static void ExtractToDirectory(System.IO.Stream source, string destinationDirectoryName, System.Text.Encoding? entryNameEncoding, bool overwriteFiles) { }
         public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName) { }
         public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, bool overwriteFiles) { }
         public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, System.Text.Encoding? entryNameEncoding) { }
index 3770da2..08606ff 100644 (file)
@@ -1,4 +1,5 @@
-<root>
+<?xml version="1.0" encoding="utf-8"?>
+<root>
   <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
     <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
     <xsd:element name="root" msdata:IsDataSet="true">
   <data name="ZipUnsupportedFile" xml:space="preserve">
     <value>The file type of '{0}' is not supported for zip archiving.</value>
   </data>
+  <data name="UnreadableStream" xml:space="preserve">
+    <value>The stream is unreadable.</value>
+  </data>
+  <data name="UnwritableStream" xml:space="preserve">
+    <value>The stream is unwritable.</value>
+  </data>
 </root>
index 4f9915e..ba088ab 100644 (file)
@@ -1,9 +1,6 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System.Buffers;
-using System.Collections.Generic;
-using System.Diagnostics;
 using System.Text;
 using System.IO.Enumeration;
 
@@ -352,6 +349,87 @@ namespace System.IO.Compression
                                                CompressionLevel compressionLevel, bool includeBaseDirectory, Encoding? entryNameEncoding) =>
             DoCreateFromDirectory(sourceDirectoryName, destinationArchiveFileName, compressionLevel, includeBaseDirectory, entryNameEncoding);
 
+        /// <summary>
+        /// Creates a zip archive in the specified stream that contains the files and directories from the specified directory.
+        /// </summary>
+        /// <param name="sourceDirectoryName">The path to the directory to be archived, specified as a relative or absolute path. A relative path is interpreted as relative to the current working directory.</param>
+        /// <param name="destination">The stream where the zip archive is to be stored.</param>
+        /// <remarks>
+        /// The directory structure from the file system is preserved in the archive. If the directory is empty, an empty archive is created.
+        /// This method overload does not include the base directory in the archive and does not allow you to specify a compression level.
+        /// If you want to include the base directory or specify a compression level, call the <see cref="CreateFromDirectory(string, Stream, CompressionLevel, bool)"/> method overload.
+        /// If a file in the directory cannot be added to the archive, the archive is left incomplete and invalid, and the method throws an <see cref="IOException"/> exception.
+        /// </remarks>
+        /// <exception cref="ArgumentException"><paramref name="sourceDirectoryName" /> is <see cref="string.Empty" />, contains only white space, or contains at least one invalid character.
+        /// -or-
+        /// The <paramref name="destination"/> stream does not support writing.
+        /// </exception>
+        /// <exception cref="ArgumentNullException"><paramref name="sourceDirectoryName" /> or <paramref name="destination" /> is <see langword="null" />.</exception>
+        /// <exception cref="PathTooLongException">In <paramref name="sourceDirectoryName" /> the specified path, file name, or both exceed the system-defined maximum length.</exception>
+        /// <exception cref="DirectoryNotFoundException"><paramref name="sourceDirectoryName" /> is invalid or does not exist (for example, it is on an unmapped drive).</exception>
+        /// <exception cref="IOException">A file in the specified directory could not be opened.
+        ///-or-
+        ///An I/O error occurred while opening a file to be archived.</exception>
+        /// <exception cref="NotSupportedException"><paramref name="sourceDirectoryName" /> contains an invalid format.</exception>
+        public static void CreateFromDirectory(string sourceDirectoryName, Stream destination) =>
+           DoCreateFromDirectory(sourceDirectoryName, destination, compressionLevel: null, includeBaseDirectory: false, entryNameEncoding: null);
+
+        /// <summary>
+        /// Creates a zip archive in the specified stream that contains the files and directories from the specified directory, uses the specified compression level, and optionally includes the base directory.
+        /// </summary>
+        /// <param name="sourceDirectoryName">The path to the directory to be archived, specified as a relative or absolute path. A relative path is interpreted as relative to the current working directory.</param>
+        /// <param name="destination">The stream where the zip archive is to be stored.</param>
+        /// <param name="compressionLevel">One of the enumeration values that indicates whether to emphasize speed or compression effectiveness when creating the entry.</param>
+        /// <param name="includeBaseDirectory"><see langword="true" /> to include the directory name from <paramref name="sourceDirectoryName" /> at the root of the archive; <see langword="false" /> to include only the contents of the directory.</param>
+        /// <remarks>
+        /// The directory structure from the file system is preserved in the archive. If the directory is empty, an empty archive is created.
+        /// Use this method overload to specify the compression level and whether to include the base directory in the archive.
+        /// If a file in the directory cannot be added to the archive, the archive is left incomplete and invalid, and the method throws an <see cref="IOException"/> exception.
+        /// </remarks>
+        /// <exception cref="ArgumentException"><paramref name="sourceDirectoryName" /> is <see cref="string.Empty" />, contains only white space, or contains at least one invalid character.
+        /// -or-
+        /// The <paramref name="destination"/> stream does not support writing.
+        /// </exception>
+        /// <exception cref="ArgumentNullException"><paramref name="sourceDirectoryName" /> or <paramref name="destination" /> is <see langword="null" />.</exception>
+        /// <exception cref="PathTooLongException">In <paramref name="sourceDirectoryName" /> the specified path, file name, or both exceed the system-defined maximum length.</exception>
+        /// <exception cref="DirectoryNotFoundException"><paramref name="sourceDirectoryName" /> is invalid or does not exist (for example, it is on an unmapped drive).</exception>
+        /// <exception cref="IOException">A file in the specified directory could not be opened.
+        ///-or-
+        ///An I/O error occurred while opening a file to be archived.</exception>
+        /// <exception cref="NotSupportedException"><paramref name="sourceDirectoryName" /> contains an invalid format.</exception>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="compressionLevel"/> is not a valid <see cref="CompressionLevel"/> value.</exception>
+        public static void CreateFromDirectory(string sourceDirectoryName, Stream destination, CompressionLevel compressionLevel, bool includeBaseDirectory) =>
+            DoCreateFromDirectory(sourceDirectoryName, destination, compressionLevel, includeBaseDirectory, entryNameEncoding: null);
+
+        /// <summary>
+        /// Creates a zip archive in the specified stream that contains the files and directories from the specified directory, uses the specified compression level and character encoding for entry names, and optionally includes the base directory.
+        /// </summary>
+        /// <param name="sourceDirectoryName">The path to the directory to be archived, specified as a relative or absolute path. A relative path is interpreted as relative to the current working directory.</param>
+        /// <param name="destination">The stream where the zip archive is to be stored.</param>
+        /// <param name="compressionLevel">One of the enumeration values that indicates whether to emphasize speed or compression effectiveness when creating the entry.</param>
+        /// <param name="includeBaseDirectory"><see langword="true" /> to include the directory name from <paramref name="sourceDirectoryName" /> at the root of the archive; <see langword="false" /> to include only the contents of the directory.</param>
+        /// <param name="entryNameEncoding">The encoding to use when reading or writing entry names in this archive. Specify a value for this parameter only when an encoding is required for interoperability with zip archive tools and libraries that do not support UTF-8 encoding for entry names.</param>
+        /// <remarks>
+        /// The directory structure from the file system is preserved in the archive. If the directory is empty, an empty archive is created.
+        /// Use this method overload to specify the compression level and character encoding, and whether to include the base directory in the archive.
+        /// If a file in the directory cannot be added to the archive, the archive is left incomplete and invalid, and the method throws an <see cref="IOException"/> exception.
+        /// </remarks>
+        /// <exception cref="ArgumentException"><paramref name="sourceDirectoryName" /> is <see cref="string.Empty" />, contains only white space, or contains at least one invalid character.
+        /// -or-
+        /// The <paramref name="destination"/> stream does not support writing.
+        /// </exception>
+        /// <exception cref="ArgumentNullException"><paramref name="sourceDirectoryName" /> or <paramref name="destination" /> is <see langword="null" />.</exception>
+        /// <exception cref="PathTooLongException">In <paramref name="sourceDirectoryName" /> the specified path, file name, or both exceed the system-defined maximum length.</exception>
+        /// <exception cref="DirectoryNotFoundException"><paramref name="sourceDirectoryName" /> is invalid or does not exist (for example, it is on an unmapped drive).</exception>
+        /// <exception cref="IOException">A file in the specified directory could not be opened.
+        ///-or-
+        ///An I/O error occurred while opening a file to be archived.</exception>
+        /// <exception cref="NotSupportedException"><paramref name="sourceDirectoryName" /> contains an invalid format.</exception>
+        /// <exception cref="ArgumentOutOfRangeException"><paramref name="compressionLevel"/> is not a valid <see cref="CompressionLevel"/> value.</exception>
+        public static void CreateFromDirectory(string sourceDirectoryName, Stream destination,
+                                               CompressionLevel compressionLevel, bool includeBaseDirectory, Encoding? entryNameEncoding) =>
+            DoCreateFromDirectory(sourceDirectoryName, destination, compressionLevel, includeBaseDirectory, entryNameEncoding);
+
         private static void DoCreateFromDirectory(string sourceDirectoryName, string destinationArchiveFileName,
                                                   CompressionLevel? compressionLevel, bool includeBaseDirectory, Encoding? entryNameEncoding)
 
@@ -364,53 +442,81 @@ namespace System.IO.Compression
             sourceDirectoryName = Path.GetFullPath(sourceDirectoryName);
             destinationArchiveFileName = Path.GetFullPath(destinationArchiveFileName);
 
-            using (ZipArchive archive = Open(destinationArchiveFileName, ZipArchiveMode.Create, entryNameEncoding))
+            using ZipArchive archive = Open(destinationArchiveFileName, ZipArchiveMode.Create, entryNameEncoding);
+            CreateZipArchiveFromDirectory(sourceDirectoryName, archive, compressionLevel, includeBaseDirectory);
+        }
+
+        private static void DoCreateFromDirectory(string sourceDirectoryName, Stream destination,
+                                                  CompressionLevel? compressionLevel, bool includeBaseDirectory, Encoding? entryNameEncoding)
+        {
+            ArgumentNullException.ThrowIfNull(destination);
+            if (!destination.CanWrite)
+            {
+                throw new ArgumentException(SR.UnwritableStream, nameof(destination));
+            }
+            if (compressionLevel.HasValue && !Enum.IsDefined(compressionLevel.Value))
             {
-                bool directoryIsEmpty = true;
+                throw new ArgumentOutOfRangeException(nameof(compressionLevel));
+            }
+
+            // Rely on Path.GetFullPath for validation of sourceDirectoryName and destinationArchive
+
+            // Checking of compressionLevel is passed down to DeflateStream and the IDeflater implementation
+            // as it is a pluggable component that completely encapsulates the meaning of compressionLevel.
+
+            sourceDirectoryName = Path.GetFullPath(sourceDirectoryName);
 
-                //add files and directories
-                DirectoryInfo di = new DirectoryInfo(sourceDirectoryName);
+            using ZipArchive archive = new ZipArchive(destination, ZipArchiveMode.Create, leaveOpen: true, entryNameEncoding);
+            CreateZipArchiveFromDirectory(sourceDirectoryName, archive, compressionLevel, includeBaseDirectory);
+        }
+
+        private static void CreateZipArchiveFromDirectory(string sourceDirectoryName, ZipArchive archive,
+                                                          CompressionLevel? compressionLevel, bool includeBaseDirectory)
+        {
+            bool directoryIsEmpty = true;
+
+            //add files and directories
+            DirectoryInfo di = new DirectoryInfo(sourceDirectoryName);
 
-                string basePath = di.FullName;
+            string basePath = di.FullName;
 
-                if (includeBaseDirectory && di.Parent != null)
-                    basePath = di.Parent.FullName;
+            if (includeBaseDirectory && di.Parent != null)
+                basePath = di.Parent.FullName;
 
-                FileSystemEnumerable<(string, CreateEntryType)> fse = CreateEnumerableForCreate(di.FullName);
+            FileSystemEnumerable<(string, CreateEntryType)> fse = CreateEnumerableForCreate(di.FullName);
 
-                foreach ((string fullPath, CreateEntryType type) in fse)
+            foreach ((string fullPath, CreateEntryType type) in fse)
+            {
+                directoryIsEmpty = false;
+
+                switch (type)
                 {
-                    directoryIsEmpty = false;
-
-                    switch (type)
-                    {
-                        case CreateEntryType.File:
-                            {
-                                // Create entry for file:
-                                string entryName = ArchivingUtils.EntryFromPath(fullPath.AsSpan(basePath.Length));
-                                ZipFileExtensions.DoCreateEntryFromFile(archive, fullPath, entryName, compressionLevel);
-                            }
-                            break;
-                        case CreateEntryType.Directory:
-                            if (ArchivingUtils.IsDirEmpty(fullPath))
-                            {
-                                // Create entry marking an empty dir:
-                                // FullName never returns a directory separator character on the end,
-                                // but Zip archives require it to specify an explicit directory:
-                                string entryName = ArchivingUtils.EntryFromPath(fullPath.AsSpan(basePath.Length), appendPathSeparator: true);
-                                archive.CreateEntry(entryName);
-                            }
-                            break;
-                        case CreateEntryType.Unsupported:
-                        default:
-                            throw new IOException(SR.Format(SR.ZipUnsupportedFile, fullPath));
-                    }
+                    case CreateEntryType.File:
+                        {
+                            // Create entry for file:
+                            string entryName = ArchivingUtils.EntryFromPath(fullPath.AsSpan(basePath.Length));
+                            ZipFileExtensions.DoCreateEntryFromFile(archive, fullPath, entryName, compressionLevel);
+                        }
+                        break;
+                    case CreateEntryType.Directory:
+                        if (ArchivingUtils.IsDirEmpty(fullPath))
+                        {
+                            // Create entry marking an empty dir:
+                            // FullName never returns a directory separator character on the end,
+                            // but Zip archives require it to specify an explicit directory:
+                            string entryName = ArchivingUtils.EntryFromPath(fullPath.AsSpan(basePath.Length), appendPathSeparator: true);
+                            archive.CreateEntry(entryName);
+                        }
+                        break;
+                    case CreateEntryType.Unsupported:
+                    default:
+                        throw new IOException(SR.Format(SR.ZipUnsupportedFile, fullPath));
                 }
-
-                // If no entries create an empty root directory entry:
-                if (includeBaseDirectory && directoryIsEmpty)
-                    archive.CreateEntry(ArchivingUtils.EntryFromPath(di.Name, appendPathSeparator: true));
             }
+
+            // If no entries create an empty root directory entry:
+            if (includeBaseDirectory && directoryIsEmpty)
+                archive.CreateEntry(ArchivingUtils.EntryFromPath(di.Name, appendPathSeparator: true));
         }
 
         private enum CreateEntryType
index 00fa93b..64662bf 100644 (file)
@@ -1,9 +1,6 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System.Buffers;
-using System.Collections.Generic;
-using System.Diagnostics;
 using System.Text;
 
 namespace System.IO.Compression
@@ -190,5 +187,146 @@ namespace System.IO.Compression
                 archive.ExtractToDirectory(destinationDirectoryName, overwriteFiles);
             }
         }
+
+        /// <summary>
+        /// Extracts all the files from the zip archive stored in the specified stream and places them in the specified destination directory on the file system.
+        /// </summary>
+        /// <param name="source">The stream from which the zip archive is to be extracted.</param>
+        /// <param name="destinationDirectoryName">The path to the directory in which to place the extracted files, specified as a relative or absolute path. A relative path is interpreted as relative to the current working directory.</param>
+        /// <remarks> This method creates the specified directory and all subdirectories. The destination directory cannot already exist.
+        /// Exceptions related to validating the paths in the <paramref name="destinationDirectoryName"/> or the files in the zip archive contained in <paramref name="source"/> parameters are thrown before extraction. Otherwise, if an error occurs during extraction, the archive remains partially extracted.
+        /// Each extracted file has the same relative path to the directory specified by <paramref name="destinationDirectoryName"/> as its source entry has to the root of the archive.
+        /// If a file to be archived has an invalid last modified time, the first date and time representable in the Zip timestamp format (midnight on January 1, 1980) will be used.</remarks>
+        /// <exception cref="ArgumentException"><paramref name="destinationDirectoryName" />> is <see cref="string.Empty" />, contains only white space, or contains at least one invalid character.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="destinationDirectoryName" /> or <paramref name="source" /> is <see langword="null" />.</exception>
+        /// <exception cref="PathTooLongException">The specified path in <paramref name="destinationDirectoryName" /> exceeds the system-defined maximum length.</exception>
+        /// <exception cref="DirectoryNotFoundException">The specified path is invalid (for example, it is on an unmapped drive).</exception>
+        /// <exception cref="IOException">The name of an entry in the archive is <see cref="string.Empty" />, contains only white space, or contains at least one invalid character.
+        /// -or-
+        /// Extracting an archive entry would create a file that is outside the directory specified by <paramref name="destinationDirectoryName" />. (For example, this might happen if the entry name contains parent directory accessors.)
+        /// -or-
+        /// An archive entry to extract has the same name as an entry that has already been extracted or that exists in <paramref name="destinationDirectoryName" />.</exception>
+        /// <exception cref="UnauthorizedAccessException">The caller does not have the required permission to access the archive or the destination directory.</exception>
+        /// <exception cref="NotSupportedException"><paramref name="destinationDirectoryName" /> contains an invalid format.</exception>
+        /// <exception cref="InvalidDataException">The archive contained in the <paramref name="source" /> stream is not a valid zip archive.
+        /// -or-
+        /// An archive entry was not found or was corrupt.
+        /// -or-
+        /// An archive entry was compressed by using a compression method that is not supported.</exception>
+        public static void ExtractToDirectory(Stream source, string destinationDirectoryName) =>
+            ExtractToDirectory(source, destinationDirectoryName, entryNameEncoding: null, overwriteFiles: false);
+
+        /// <summary>
+        /// Extracts all the files from the zip archive stored in the specified stream and places them in the specified destination directory on the file system, and optionally allows choosing if the files in the destination directory should be overwritten.
+        /// </summary>
+        /// <param name="source">The stream from which the zip archive is to be extracted.</param>
+        /// <param name="destinationDirectoryName">The path to the directory in which to place the extracted files, specified as a relative or absolute path. A relative path is interpreted as relative to the current working directory.</param>
+        /// <param name="overwriteFiles"><see langword="true" /> to overwrite files; <see langword="false" /> otherwise.</param>
+        /// <remarks> This method creates the specified directory and all subdirectories. The destination directory cannot already exist.
+        /// Exceptions related to validating the paths in the <paramref name="destinationDirectoryName"/> or the files in the zip archive contained in <paramref name="source"/> parameters are thrown before extraction. Otherwise, if an error occurs during extraction, the archive remains partially extracted.
+        /// Each extracted file has the same relative path to the directory specified by <paramref name="destinationDirectoryName"/> as its source entry has to the root of the archive.
+        /// If a file to be archived has an invalid last modified time, the first date and time representable in the Zip timestamp format (midnight on January 1, 1980) will be used.</remarks>
+        /// <exception cref="ArgumentException"><paramref name="destinationDirectoryName" />> is <see cref="string.Empty" />, contains only white space, or contains at least one invalid character.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="destinationDirectoryName" /> or <paramref name="source" /> is <see langword="null" />.</exception>
+        /// <exception cref="PathTooLongException">The specified path in <paramref name="destinationDirectoryName" /> exceeds the system-defined maximum length.</exception>
+        /// <exception cref="DirectoryNotFoundException">The specified path is invalid (for example, it is on an unmapped drive).</exception>
+        /// <exception cref="IOException">The name of an entry in the archive is <see cref="string.Empty" />, contains only white space, or contains at least one invalid character.
+        /// -or-
+        /// Extracting an archive entry would create a file that is outside the directory specified by <paramref name="destinationDirectoryName" />. (For example, this might happen if the entry name contains parent directory accessors.)
+        /// -or-
+        /// <paramref name="overwriteFiles" /> is <see langword="false" /> and an archive entry to extract has the same name as an entry that has already been extracted or that exists in <paramref name="destinationDirectoryName" />.</exception>
+        /// <exception cref="UnauthorizedAccessException">The caller does not have the required permission to access the archive or the destination directory.</exception>
+        /// <exception cref="NotSupportedException"><paramref name="destinationDirectoryName" /> contains an invalid format.</exception>
+        /// <exception cref="InvalidDataException">The archive contained in the <paramref name="source" /> stream is not a valid zip archive.
+        /// -or-
+        /// An archive entry was not found or was corrupt.
+        /// -or-
+        /// An archive entry was compressed by using a compression method that is not supported.</exception>
+        public static void ExtractToDirectory(Stream source, string destinationDirectoryName, bool overwriteFiles) =>
+            ExtractToDirectory(source, destinationDirectoryName, entryNameEncoding: null, overwriteFiles: overwriteFiles);
+
+        /// <summary>
+        /// Extracts all the files from the zip archive stored in the specified stream and places them in the specified destination directory on the file system and uses the specified character encoding for entry names.
+        /// </summary>
+        /// <param name="source">The stream from which the zip archive is to be extracted.</param>
+        /// <param name="destinationDirectoryName">The path to the directory in which to place the extracted files, specified as a relative or absolute path. A relative path is interpreted as relative to the current working directory.</param>
+        /// <param name="entryNameEncoding">The encoding to use when reading or writing entry names in this archive. Specify a value for this parameter only when an encoding is required for interoperability with zip archive tools and libraries that do not support UTF-8 encoding for entry names.</param>
+        /// <remarks> This method creates the specified directory and all subdirectories. The destination directory cannot already exist.
+        /// Exceptions related to validating the paths in the <paramref name="destinationDirectoryName"/> or the files in the zip archive contained in <paramref name="source"/> parameters are thrown before extraction. Otherwise, if an error occurs during extraction, the archive remains partially extracted.
+        /// Each extracted file has the same relative path to the directory specified by <paramref name="destinationDirectoryName"/> as its source entry has to the root of the archive.
+        /// If a file to be archived has an invalid last modified time, the first date and time representable in the Zip timestamp format (midnight on January 1, 1980) will be used.</remarks>
+        /// If <paramref name="entryNameEncoding"/> is set to a value other than <see langword="null"/>, entry names are decoded according to the following rules:
+        /// - For entry names where the language encoding flag (in the general-purpose bit flag of the local file header) is not set, the entry names are decoded by using the specified encoding.
+        /// - For entries where the language encoding flag is set, the entry names are decoded by using UTF-8.
+        /// If <paramref name="entryNameEncoding"/> is set to <see langword="null"/>, entry names are decoded according to the following rules:
+        /// - For entries where the language encoding flag (in the general-purpose bit flag of the local file header) is not set, entry names are decoded by using the current system default code page.
+        /// - For entries where the language encoding flag is set, the entry names are decoded by using UTF-8.
+        /// <exception cref="ArgumentException"><paramref name="destinationDirectoryName" />> is <see cref="string.Empty" />, contains only white space, or contains at least one invalid character.
+        /// -or-
+        /// <paramref name="entryNameEncoding"/> is set to a Unicode encoding other than UTF-8.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="destinationDirectoryName" /> or <paramref name="source" /> is <see langword="null" />.</exception>
+        /// <exception cref="PathTooLongException">The specified path in <paramref name="destinationDirectoryName" /> exceeds the system-defined maximum length.</exception>
+        /// <exception cref="DirectoryNotFoundException">The specified path is invalid (for example, it is on an unmapped drive).</exception>
+        /// <exception cref="IOException">The name of an entry in the archive is <see cref="string.Empty" />, contains only white space, or contains at least one invalid character.
+        /// -or-
+        /// Extracting an archive entry would create a file that is outside the directory specified by <paramref name="destinationDirectoryName" />. (For example, this might happen if the entry name contains parent directory accessors.)
+        /// -or-
+        /// An archive entry to extract has the same name as an entry that has already been extracted or that exists in <paramref name="destinationDirectoryName" />.</exception>
+        /// <exception cref="UnauthorizedAccessException">The caller does not have the required permission to access the archive or the destination directory.</exception>
+        /// <exception cref="NotSupportedException"><paramref name="destinationDirectoryName" /> contains an invalid format.</exception>
+        /// <exception cref="InvalidDataException">The archive contained in the <paramref name="source" /> stream is not a valid zip archive.
+        /// -or-
+        /// An archive entry was not found or was corrupt.
+        /// -or-
+        /// An archive entry was compressed by using a compression method that is not supported.</exception>
+        public static void ExtractToDirectory(Stream source, string destinationDirectoryName, Encoding? entryNameEncoding) =>
+            ExtractToDirectory(source, destinationDirectoryName, entryNameEncoding: entryNameEncoding, overwriteFiles: false);
+
+        /// <summary>
+        /// Extracts all the files from the zip archive stored in the specified stream and places them in the specified destination directory on the file system, uses the specified character encoding for entry names, and optionally allows choosing if the files in the destination directory should be overwritten.
+        /// </summary>
+        /// <param name="source">The stream from which the zip archive is to be extracted.</param>
+        /// <param name="destinationDirectoryName">The path to the directory in which to place the extracted files, specified as a relative or absolute path. A relative path is interpreted as relative to the current working directory.</param>
+        /// <param name="entryNameEncoding">The encoding to use when reading or writing entry names in this archive. Specify a value for this parameter only when an encoding is required for interoperability with zip archive tools and libraries that do not support UTF-8 encoding for entry names.</param>
+        /// <param name="overwriteFiles"><see langword="true" /> to overwrite files; <see langword="false" /> otherwise.</param>
+        /// <remarks> This method creates the specified directory and all subdirectories. The destination directory cannot already exist.
+        /// Exceptions related to validating the paths in the <paramref name="destinationDirectoryName"/> or the files in the zip archive contained in <paramref name="source"/> parameters are thrown before extraction. Otherwise, if an error occurs during extraction, the archive remains partially extracted.
+        /// Each extracted file has the same relative path to the directory specified by <paramref name="destinationDirectoryName"/> as its source entry has to the root of the archive.
+        /// If a file to be archived has an invalid last modified time, the first date and time representable in the Zip timestamp format (midnight on January 1, 1980) will be used.</remarks>
+        /// If <paramref name="entryNameEncoding"/> is set to a value other than <see langword="null"/>, entry names are decoded according to the following rules:
+        /// - For entry names where the language encoding flag (in the general-purpose bit flag of the local file header) is not set, the entry names are decoded by using the specified encoding.
+        /// - For entries where the language encoding flag is set, the entry names are decoded by using UTF-8.
+        /// If <paramref name="entryNameEncoding"/> is set to <see langword="null"/>, entry names are decoded according to the following rules:
+        /// - For entries where the language encoding flag (in the general-purpose bit flag of the local file header) is not set, entry names are decoded by using the current system default code page.
+        /// - For entries where the language encoding flag is set, the entry names are decoded by using UTF-8.
+        /// <exception cref="ArgumentException"><paramref name="destinationDirectoryName" />> is <see cref="string.Empty" />, contains only white space, or contains at least one invalid character.
+        /// -or-
+        /// <paramref name="entryNameEncoding"/> is set to a Unicode encoding other than UTF-8.</exception>
+        /// <exception cref="ArgumentNullException"><paramref name="destinationDirectoryName" /> or <paramref name="source" /> is <see langword="null" />.</exception>
+        /// <exception cref="PathTooLongException">The specified path in <paramref name="destinationDirectoryName" /> exceeds the system-defined maximum length.</exception>
+        /// <exception cref="DirectoryNotFoundException">The specified path is invalid (for example, it is on an unmapped drive).</exception>
+        /// <exception cref="IOException">The name of an entry in the archive is <see cref="string.Empty" />, contains only white space, or contains at least one invalid character.
+        /// -or-
+        /// Extracting an archive entry would create a file that is outside the directory specified by <paramref name="destinationDirectoryName" />. (For example, this might happen if the entry name contains parent directory accessors.)
+        /// -or-
+        /// <paramref name="overwriteFiles" /> is <see langword="false" /> and an archive entry to extract has the same name as an entry that has already been extracted or that exists in <paramref name="destinationDirectoryName" />.</exception>
+        /// <exception cref="UnauthorizedAccessException">The caller does not have the required permission to access the archive or the destination directory.</exception>
+        /// <exception cref="NotSupportedException"><paramref name="destinationDirectoryName" /> contains an invalid format.</exception>
+        /// <exception cref="InvalidDataException">The archive contained in the <paramref name="source" /> stream is not a valid zip archive.
+        /// -or-
+        /// An archive entry was not found or was corrupt.
+        /// -or-
+        /// An archive entry was compressed by using a compression method that is not supported.</exception>
+        public static void ExtractToDirectory(Stream source, string destinationDirectoryName, Encoding? entryNameEncoding, bool overwriteFiles)
+        {
+            ArgumentNullException.ThrowIfNull(source);
+            if (!source.CanRead)
+            {
+                throw new ArgumentException(SR.UnreadableStream, nameof(source));
+            }
+
+            using ZipArchive archive = new ZipArchive(source, ZipArchiveMode.Read, leaveOpen: true, entryNameEncoding);
+            archive.ExtractToDirectory(destinationDirectoryName, overwriteFiles);
+        }
     }
 }
index 4fbc658..be0d1f4 100644 (file)
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <EnableLibraryImportGenerator>true</EnableLibraryImportGenerator>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
 
   <ItemGroup>
     <Compile Include="ZipFile.Create.cs" />
+    <Compile Include="ZipFile.Create.Stream.cs" />
     <Compile Include="ZipFile.Extract.cs" />
+    <Compile Include="ZipFile.Extract.Stream.cs" />
+    <Compile Include="ZipFile.Open.cs" />
     <Compile Include="ZipFileExtensions.ZipArchive.Create.cs" />
     <Compile Include="ZipFileExtensions.ZipArchiveEntry.Extract.cs" />
     <Compile Include="ZipFileExtensions.ZipArchive.Extract.cs" />
@@ -26,6 +29,8 @@
              Link="Common\System\IO\TempFile.cs" />
     <Compile Include="$(CommonTestPath)System\IO\TempDirectory.cs"
              Link="Common\System\IO\TempDirectory.cs" />
+    <Compile Include="$(CommonTestPath)System\IO\WrappedStream.cs"
+             Link="Common\System\IO\WrappedStream.cs" />
     <Compile Include="$(CommonTestPath)System\IO\Compression\CRC.cs"
              Link="Common\System\IO\Compression\CRC.cs" />
     <Compile Include="$(CommonTestPath)System\IO\Compression\FileData.cs"
index 5e63b81..d4c40f6 100644 (file)
@@ -91,5 +91,25 @@ namespace System.IO.Compression.Tests
                 }
             }
         }
+
+        [Fact]
+        public void ExtractToDirectoryZipArchiveOverwrite()
+        {
+            string zipFileName = zfile("normal.zip");
+            string folderName = zfolder("normal");
+
+            using (var tempFolder = new TempDirectory(GetTestFilePath()))
+            {
+                using (ZipArchive archive = ZipFile.Open(zipFileName, ZipArchiveMode.Read))
+                {
+                    archive.ExtractToDirectory(tempFolder.Path);
+                    Assert.Throws<IOException>(() => archive.ExtractToDirectory(tempFolder.Path /* default false */));
+                    Assert.Throws<IOException>(() => archive.ExtractToDirectory(tempFolder.Path, overwriteFiles: false));
+                    archive.ExtractToDirectory(tempFolder.Path, overwriteFiles: true);
+
+                    DirsEqual(tempFolder.Path, folderName);
+                }
+            }
+        }
     }
 }
diff --git a/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Create.Stream.cs b/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Create.Stream.cs
new file mode 100644 (file)
index 0000000..852545a
--- /dev/null
@@ -0,0 +1,172 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.IO.Compression.Tests;
+
+public class ZipFile_Create_Stream : ZipFileTestBase
+{
+    [Fact]
+    public void CreateFromDirectory_NullSourceDirectory_Throws()
+    {
+        using MemoryStream ms = new MemoryStream();
+        Assert.Throws<ArgumentNullException>(() => ZipFile.CreateFromDirectory(sourceDirectoryName: null, ms));
+        Assert.Throws<ArgumentNullException>(() => ZipFile.CreateFromDirectory(sourceDirectoryName: null, ms, CompressionLevel.NoCompression, includeBaseDirectory: false));
+        Assert.Throws<ArgumentNullException>(() => ZipFile.CreateFromDirectory(sourceDirectoryName: null, ms, CompressionLevel.NoCompression, includeBaseDirectory: false, Encoding.UTF8));
+    }
+
+    [Theory]
+    [InlineData((CompressionLevel)int.MinValue)]
+    [InlineData((CompressionLevel)(-1))]
+    [InlineData((CompressionLevel)4)]
+    [InlineData((CompressionLevel)int.MaxValue)]
+    public void CreateFromDirectory_CompressionLevel_OutOfRange_Throws(CompressionLevel invalidCompressionLevel)
+    {
+        using MemoryStream ms = new MemoryStream();
+        Assert.Throws<ArgumentOutOfRangeException>(() => ZipFile.CreateFromDirectory("sourceDirectory", ms, invalidCompressionLevel, includeBaseDirectory: false));
+        Assert.Throws<ArgumentOutOfRangeException>(() => ZipFile.CreateFromDirectory("sourceDirectory", ms, invalidCompressionLevel, includeBaseDirectory: false, Encoding.UTF8));
+    }
+       
+    [Fact]
+    public void CreateFromDirectory_UnwritableStream_Throws()
+    {
+        using MemoryStream ms = new();
+        using WrappedStream destination = new(ms, canRead: true, canWrite: false, canSeek: true);
+        Assert.Throws<ArgumentException>("destination", () => ZipFile.CreateFromDirectory(GetTestFilePath(), destination));
+    }
+
+    [Fact]
+    public void CreateFromDirectoryNormal()
+    {
+        string folderName = zfolder("normal");
+        using MemoryStream destination = new();
+        ZipFile.CreateFromDirectory(folderName, destination);
+        destination.Position = 0;
+        IsZipSameAsDir(destination, folderName, ZipArchiveMode.Read, requireExplicit: false, checkTimes: false);
+    }
+
+    [Fact]
+    public void CreateFromDirectoryNormal_Unreadable_Unseekable()
+    {
+        string folderName = zfolder("normal");
+        using MemoryStream ms = new();
+        using WrappedStream destination = new(ms, canRead: false, canWrite: true, canSeek: false);
+        ZipFile.CreateFromDirectory(folderName, destination);
+        ms.Position = 0;
+        IsZipSameAsDir(ms, folderName, ZipArchiveMode.Read, requireExplicit: false, checkTimes: false);
+    }
+
+    [Fact]
+    public void CreateFromDirectory_IncludeBaseDirectory()
+    {
+        string folderName = zfolder("normal");
+        using MemoryStream destination = new();
+        ZipFile.CreateFromDirectory(folderName, destination, CompressionLevel.Optimal, true);
+
+        IEnumerable<string> expected = Directory.EnumerateFiles(zfolder("normal"), "*", SearchOption.AllDirectories);
+        destination.Position = 0;
+        using ZipArchive archive = new(destination);
+        foreach (ZipArchiveEntry actualEntry in archive.Entries)
+        {
+            string expectedFile = expected.Single(i => Path.GetFileName(i).Equals(actualEntry.Name));
+            Assert.StartsWith("normal", actualEntry.FullName);
+            Assert.Equal(new FileInfo(expectedFile).Length, actualEntry.Length);
+            using Stream expectedStream = File.OpenRead(expectedFile);
+            using Stream actualStream = actualEntry.Open();
+            StreamsEqual(expectedStream, actualStream);
+        }
+    }
+
+    [Fact]
+    public void CreateFromDirectoryUnicode()
+    {
+        string folderName = zfolder("unicode");
+        using MemoryStream destination = new();
+        ZipFile.CreateFromDirectory(folderName, destination);
+
+        using ZipArchive archive = new(destination);
+        IEnumerable<string> actual = archive.Entries.Select(entry => entry.Name);
+        IEnumerable<string> expected = Directory.EnumerateFileSystemEntries(zfolder("unicode"), "*", SearchOption.AllDirectories).ToList();
+        Assert.True(Enumerable.SequenceEqual(expected.Select(i => Path.GetFileName(i)), actual.Select(i => i)));
+    }
+
+    [Fact]
+    public void CreatedEmptyDirectoriesRoundtrip()
+    {
+        using TempDirectory tempFolder = new(GetTestFilePath());
+        
+        DirectoryInfo rootDir = new(tempFolder.Path);
+        rootDir.CreateSubdirectory("empty1");
+
+        using MemoryStream destination = new();
+        ZipFile.CreateFromDirectory(
+            rootDir.FullName, destination,
+            CompressionLevel.Optimal, false, Encoding.UTF8);
+
+        using ZipArchive archive = new(destination);
+
+        Assert.Equal(1, archive.Entries.Count);
+        Assert.StartsWith("empty1", archive.Entries[0].FullName);
+    }
+
+    [Fact]
+    public void CreatedEmptyUtf32DirectoriesRoundtrip()
+    {
+        using TempDirectory tempFolder = new(GetTestFilePath());
+
+        Encoding entryEncoding = Encoding.UTF32;
+        DirectoryInfo rootDir = new(tempFolder.Path);
+        rootDir.CreateSubdirectory("empty1");
+
+        using MemoryStream destination = new();
+        ZipFile.CreateFromDirectory(
+            rootDir.FullName, destination,
+            CompressionLevel.Optimal, false, entryEncoding);
+
+        using ZipArchive archive = new(destination, ZipArchiveMode.Read, leaveOpen: false, entryEncoding);
+        Assert.Equal(1, archive.Entries.Count);
+        Assert.StartsWith("empty1", archive.Entries[0].FullName);
+    }
+
+    [Fact]
+    public void CreatedEmptyRootDirectoryRoundtrips()
+    {
+        using TempDirectory tempFolder = new(GetTestFilePath());
+
+        DirectoryInfo emptyRoot = new(tempFolder.Path);
+        using MemoryStream destination = new();
+        ZipFile.CreateFromDirectory(
+            emptyRoot.FullName, destination,
+            CompressionLevel.Optimal, true);
+
+        using ZipArchive archive = new(destination);
+        Assert.Equal(1, archive.Entries.Count);
+    }
+
+    [Fact]
+    public void CreateSetsExternalAttributesCorrectly()
+    {
+        string folderName = zfolder("normal");
+        using MemoryStream destination = new();
+        ZipFile.CreateFromDirectory(folderName, destination);
+        destination.Position = 0;
+        using ZipArchive archive = new(destination);
+
+        foreach (ZipArchiveEntry entry in archive.Entries)
+        {
+            if (OperatingSystem.IsWindows())
+            {
+                Assert.Equal(0, entry.ExternalAttributes);
+            }
+            else
+            {
+                Assert.NotEqual(0, entry.ExternalAttributes);
+            }
+        }
+    }
+}
index 7f450e7..cec2dc9 100644 (file)
@@ -146,304 +146,6 @@ namespace System.IO.Compression.Tests
         }
 
         [Fact]
-        public void InvalidInstanceMethods()
-        {
-            using (TempFile testArchive = CreateTempCopyFile(zfile("normal.zip"), GetTestFilePath()))
-            using (ZipArchive archive = ZipFile.Open(testArchive.Path, ZipArchiveMode.Update))
-            {
-                //non-existent entry
-                Assert.True(null == archive.GetEntry("nonExistentEntry"));
-                //null/empty string
-                Assert.Throws<ArgumentNullException>(() => archive.GetEntry(null));
-
-                ZipArchiveEntry entry = archive.GetEntry("first.txt");
-
-                //null/empty string
-                AssertExtensions.Throws<ArgumentException>("entryName", () => archive.CreateEntry(""));
-                Assert.Throws<ArgumentNullException>(() => archive.CreateEntry(null));
-            }
-        }
-
-        [Fact]
-        public void InvalidConstructors()
-        {
-            //out of range enum values
-            Assert.Throws<ArgumentOutOfRangeException>(() => ZipFile.Open("bad file", (ZipArchiveMode)(10)));
-        }
-
-        [Fact]
-        public void InvalidFiles()
-        {
-            Assert.Throws<InvalidDataException>(() => ZipFile.OpenRead(bad("EOCDmissing.zip")));
-            using (TempFile testArchive = CreateTempCopyFile(bad("EOCDmissing.zip"), GetTestFilePath()))
-            {
-                Assert.Throws<InvalidDataException>(() => ZipFile.Open(testArchive.Path, ZipArchiveMode.Update));
-            }
-
-            Assert.Throws<InvalidDataException>(() => ZipFile.OpenRead(bad("CDoffsetOutOfBounds.zip")));
-            using (TempFile testArchive = CreateTempCopyFile(bad("CDoffsetOutOfBounds.zip"), GetTestFilePath()))
-            {
-                Assert.Throws<InvalidDataException>(() => ZipFile.Open(testArchive.Path, ZipArchiveMode.Update));
-            }
-
-            using (ZipArchive archive = ZipFile.OpenRead(bad("CDoffsetInBoundsWrong.zip")))
-            {
-                Assert.Throws<InvalidDataException>(() => { var x = archive.Entries; });
-            }
-
-            using (TempFile testArchive = CreateTempCopyFile(bad("CDoffsetInBoundsWrong.zip"), GetTestFilePath()))
-            {
-                Assert.Throws<InvalidDataException>(() => ZipFile.Open(testArchive.Path, ZipArchiveMode.Update));
-            }
-
-            using (ZipArchive archive = ZipFile.OpenRead(bad("numberOfEntriesDifferent.zip")))
-            {
-                Assert.Throws<InvalidDataException>(() => { var x = archive.Entries; });
-            }
-            using (TempFile testArchive = CreateTempCopyFile(bad("numberOfEntriesDifferent.zip"), GetTestFilePath()))
-            {
-                Assert.Throws<InvalidDataException>(() => ZipFile.Open(testArchive.Path, ZipArchiveMode.Update));
-            }
-
-            //read mode on empty file
-            using (var memoryStream = new MemoryStream())
-            {
-                Assert.Throws<InvalidDataException>(() => new ZipArchive(memoryStream));
-            }
-
-            //offset out of bounds
-            using (ZipArchive archive = ZipFile.OpenRead(bad("localFileOffsetOutOfBounds.zip")))
-            {
-                ZipArchiveEntry e = archive.Entries[0];
-                Assert.Throws<InvalidDataException>(() => e.Open());
-            }
-
-            using (TempFile testArchive = CreateTempCopyFile(bad("localFileOffsetOutOfBounds.zip"), GetTestFilePath()))
-            {
-                Assert.Throws<InvalidDataException>(() => ZipFile.Open(testArchive.Path, ZipArchiveMode.Update));
-            }
-
-            //compressed data offset + compressed size out of bounds
-            using (ZipArchive archive = ZipFile.OpenRead(bad("compressedSizeOutOfBounds.zip")))
-            {
-                ZipArchiveEntry e = archive.Entries[0];
-                Assert.Throws<InvalidDataException>(() => e.Open());
-            }
-
-            using (TempFile testArchive = CreateTempCopyFile(bad("compressedSizeOutOfBounds.zip"), GetTestFilePath()))
-            {
-                Assert.Throws<InvalidDataException>(() => ZipFile.Open(testArchive.Path, ZipArchiveMode.Update));
-            }
-
-            //signature wrong
-            using (ZipArchive archive = ZipFile.OpenRead(bad("localFileHeaderSignatureWrong.zip")))
-            {
-                ZipArchiveEntry e = archive.Entries[0];
-                Assert.Throws<InvalidDataException>(() => e.Open());
-            }
-
-            using (TempFile testArchive = CreateTempCopyFile(bad("localFileHeaderSignatureWrong.zip"), GetTestFilePath()))
-            {
-                Assert.Throws<InvalidDataException>(() => ZipFile.Open(testArchive.Path, ZipArchiveMode.Update));
-            }
-        }
-
-        [Theory]
-        [InlineData("LZMA.zip", true)]
-        [InlineData("invalidDeflate.zip", false)]
-        public void UnsupportedCompressionRoutine(string zipName, bool throwsOnOpen)
-        {
-            string filename = bad(zipName);
-            using (ZipArchive archive = ZipFile.OpenRead(filename))
-            {
-                ZipArchiveEntry e = archive.Entries[0];
-                if (throwsOnOpen)
-                {
-                    Assert.Throws<InvalidDataException>(() => e.Open());
-                }
-                else
-                {
-                    using (Stream s = e.Open())
-                    {
-                        Assert.Throws<InvalidDataException>(() => s.ReadByte());
-                    }
-                }
-            }
-
-            using (TempFile updatedCopy = CreateTempCopyFile(filename, GetTestFilePath()))
-            {
-                string name;
-                long length, compressedLength;
-                DateTimeOffset lastWriteTime;
-                using (ZipArchive archive = ZipFile.Open(updatedCopy.Path, ZipArchiveMode.Update))
-                {
-                    ZipArchiveEntry e = archive.Entries[0];
-                    name = e.FullName;
-                    lastWriteTime = e.LastWriteTime;
-                    length = e.Length;
-                    compressedLength = e.CompressedLength;
-                    Assert.Throws<InvalidDataException>(() => e.Open());
-                }
-
-                //make sure that update mode preserves that unreadable file
-                using (ZipArchive archive = ZipFile.Open(updatedCopy.Path, ZipArchiveMode.Update))
-                {
-                    ZipArchiveEntry e = archive.Entries[0];
-                    Assert.Equal(name, e.FullName);
-                    Assert.Equal(lastWriteTime, e.LastWriteTime);
-                    Assert.Equal(length, e.Length);
-                    Assert.Equal(compressedLength, e.CompressedLength);
-                    Assert.Throws<InvalidDataException>(() => e.Open());
-                }
-            }
-        }
-
-        [Fact]
-        public void InvalidDates()
-        {
-            using (ZipArchive archive = ZipFile.OpenRead(bad("invaliddate.zip")))
-            {
-                Assert.Equal(new DateTime(1980, 1, 1, 0, 0, 0), archive.Entries[0].LastWriteTime.DateTime);
-            }
-
-            // Browser VFS does not support saving file attributes, so skip
-            if (!PlatformDetection.IsBrowser)
-            {
-                FileInfo fileWithBadDate = new FileInfo(GetTestFilePath());
-                fileWithBadDate.Create().Dispose();
-                fileWithBadDate.LastWriteTimeUtc = new DateTime(1970, 1, 1, 1, 1, 1);
-                string archivePath = GetTestFilePath();
-                using (FileStream output = File.Open(archivePath, FileMode.Create))
-                using (ZipArchive archive = new ZipArchive(output, ZipArchiveMode.Create))
-                {
-                    archive.CreateEntryFromFile(fileWithBadDate.FullName, "SomeEntryName");
-                }
-                using (ZipArchive archive = ZipFile.OpenRead(archivePath))
-                {
-                    Assert.Equal(new DateTime(1980, 1, 1, 0, 0, 0), archive.Entries[0].LastWriteTime.DateTime);
-                }
-            }
-        }
-
-        [Fact]
-        public void FilesOutsideDirectory()
-        {
-            string archivePath = GetTestFilePath();
-            using (ZipArchive archive = ZipFile.Open(archivePath, ZipArchiveMode.Create))
-            using (StreamWriter writer = new StreamWriter(archive.CreateEntry(Path.Combine("..", "entry1"), CompressionLevel.Optimal).Open()))
-            {
-                writer.Write("This is a test.");
-            }
-            Assert.Throws<IOException>(() => ZipFile.ExtractToDirectory(archivePath, GetTestFilePath()));
-        }
-
-        [Fact]
-        public void DirectoryEntryWithData()
-        {
-            string archivePath = GetTestFilePath();
-            using (ZipArchive archive = ZipFile.Open(archivePath, ZipArchiveMode.Create))
-            using (StreamWriter writer = new StreamWriter(archive.CreateEntry("testdir" + Path.DirectorySeparatorChar, CompressionLevel.Optimal).Open()))
-            {
-                writer.Write("This is a test.");
-            }
-            Assert.Throws<IOException>(() => ZipFile.ExtractToDirectory(archivePath, GetTestFilePath()));
-        }
-
-        [Fact]
-        public void ReadStreamOps()
-        {
-            using (ZipArchive archive = ZipFile.OpenRead(zfile("normal.zip")))
-            {
-                foreach (ZipArchiveEntry e in archive.Entries)
-                {
-                    using (Stream s = e.Open())
-                    {
-                        Assert.True(s.CanRead, "Can read to read archive");
-                        Assert.False(s.CanWrite, "Can't write to read archive");
-                        Assert.False(s.CanSeek, "Can't seek on archive");
-                        Assert.Equal(LengthOfUnseekableStream(s), e.Length);
-                    }
-                }
-            }
-        }
-
-        [Fact]
-        public void UpdateReadTwice()
-        {
-            using (TempFile testArchive = CreateTempCopyFile(zfile("small.zip"), GetTestFilePath()))
-            using (ZipArchive archive = ZipFile.Open(testArchive.Path, ZipArchiveMode.Update))
-            {
-                ZipArchiveEntry entry = archive.Entries[0];
-                string contents1, contents2;
-                using (StreamReader s = new StreamReader(entry.Open()))
-                {
-                    contents1 = s.ReadToEnd();
-                }
-                using (StreamReader s = new StreamReader(entry.Open()))
-                {
-                    contents2 = s.ReadToEnd();
-                }
-                Assert.Equal(contents1, contents2);
-            }
-        }
-
-        [Fact]
-        public async Task UpdateAddFile()
-        {
-            //add file
-            using (TempFile testArchive = CreateTempCopyFile(zfile("normal.zip"), GetTestFilePath()))
-            {
-                using (ZipArchive archive = ZipFile.Open(testArchive.Path, ZipArchiveMode.Update))
-                {
-                    await UpdateArchive(archive, zmodified(Path.Combine("addFile", "added.txt")), "added.txt");
-                }
-                await IsZipSameAsDirAsync(testArchive.Path, zmodified("addFile"), ZipArchiveMode.Read);
-            }
-
-            //add file and read entries before
-            using (TempFile testArchive = CreateTempCopyFile(zfile("normal.zip"), GetTestFilePath()))
-            {
-                using (ZipArchive archive = ZipFile.Open(testArchive.Path, ZipArchiveMode.Update))
-                {
-                    var x = archive.Entries;
-
-                    await UpdateArchive(archive, zmodified(Path.Combine("addFile", "added.txt")), "added.txt");
-                }
-                await IsZipSameAsDirAsync(testArchive.Path, zmodified("addFile"), ZipArchiveMode.Read);
-            }
-
-            //add file and read entries after
-            using (TempFile testArchive = CreateTempCopyFile(zfile("normal.zip"), GetTestFilePath()))
-            {
-                using (ZipArchive archive = ZipFile.Open(testArchive.Path, ZipArchiveMode.Update))
-                {
-                    await UpdateArchive(archive, zmodified(Path.Combine("addFile", "added.txt")), "added.txt");
-
-                    var x = archive.Entries;
-                }
-                await IsZipSameAsDirAsync(testArchive.Path, zmodified("addFile"), ZipArchiveMode.Read);
-            }
-        }
-
-        private static async Task UpdateArchive(ZipArchive archive, string installFile, string entryName)
-        {
-            string fileName = installFile;
-            ZipArchiveEntry e = archive.CreateEntry(entryName);
-
-            var file = FileData.GetFile(fileName);
-            e.LastWriteTime = file.LastModifiedDate;
-
-            using (var stream = await StreamHelpers.CreateTempCopyStream(fileName))
-            {
-                using (Stream es = e.Open())
-                {
-                    es.SetLength(0);
-                    stream.CopyTo(es);
-                }
-            }
-        }
-
-        [Fact]
         public void CreateSetsExternalAttributesCorrectly()
         {
             string folderName = zfolder("normal");
diff --git a/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Extract.Stream.cs b/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Extract.Stream.cs
new file mode 100644 (file)
index 0000000..0905778
--- /dev/null
@@ -0,0 +1,245 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text;
+using Xunit;
+
+namespace System.IO.Compression.Tests;
+
+public class ZipFile_Extract_Stream : ZipFileTestBase
+{
+    [Fact]
+    public void ExtractToDirectory_NullStream_Throws()
+    {
+        Assert.Throws<ArgumentNullException>("source", () => ZipFile.ExtractToDirectory(source: null, GetTestFilePath()));
+    }
+
+    [Fact]
+    public void ExtractToDirectory_UnreadableStream_Throws()
+    {
+        using MemoryStream ms = new();
+        using WrappedStream source = new(ms, canRead: false, canWrite: true, canSeek: true);
+        Assert.Throws<ArgumentException>("source", () => ZipFile.ExtractToDirectory(source, GetTestFilePath()));
+    }
+
+    [Theory]
+    [InlineData("normal.zip", "normal")]
+    [InlineData("empty.zip", "empty")]
+    [InlineData("explicitdir1.zip", "explicitdir")]
+    [InlineData("explicitdir2.zip", "explicitdir")]
+    [InlineData("appended.zip", "small")]
+    [InlineData("prepended.zip", "small")]
+    [InlineData("noexplicitdir.zip", "explicitdir")]
+    public void ExtractToDirectoryNormal(string file, string folder)
+    {
+        using FileStream source = File.OpenRead(zfile(file));
+        string folderName = zfolder(folder);
+        using TempDirectory tempFolder = new(GetTestFilePath());
+        ZipFile.ExtractToDirectory(source, tempFolder.Path);
+        DirsEqual(tempFolder.Path, folderName);
+    }
+
+    [Theory]
+    [InlineData("normal.zip", "normal")]
+    [InlineData("empty.zip", "empty")]
+    [InlineData("explicitdir1.zip", "explicitdir")]
+    [InlineData("explicitdir2.zip", "explicitdir")]
+    [InlineData("appended.zip", "small")]
+    [InlineData("prepended.zip", "small")]
+    [InlineData("noexplicitdir.zip", "explicitdir")]
+    public void ExtractToDirectoryNormal_Unwritable_Unseekable(string file, string folder)
+    {
+        using FileStream fs = File.OpenRead(zfile(file));
+        using WrappedStream source = new(fs, canRead: true, canWrite: false, canSeek: false);
+        string folderName = zfolder(folder);
+        using TempDirectory tempFolder = new(GetTestFilePath());
+        ZipFile.ExtractToDirectory(source, tempFolder.Path);
+        DirsEqual(tempFolder.Path, folderName);
+    }
+
+    [Fact]
+    [ActiveIssue("https://github.com/dotnet/runtime/issues/72951", TestPlatforms.iOS | TestPlatforms.tvOS)]
+    public void ExtractToDirectoryUnicode()
+    {
+        using Stream source = File.OpenRead(zfile("unicode.zip"));
+        string folderName = zfolder("unicode");
+        using TempDirectory tempFolder = new TempDirectory(GetTestFilePath());
+        ZipFile.ExtractToDirectory(source, tempFolder.Path);
+        DirFileNamesEqual(tempFolder.Path, folderName);
+    }
+
+    [Theory]
+    [InlineData("../Foo")]
+    [InlineData("../Barbell")]
+    public void ExtractOutOfRoot(string entryName)
+    {
+        using FileStream source = new(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite);
+        using (ZipArchive archive = new(source, ZipArchiveMode.Create, leaveOpen: true))
+        {
+            ZipArchiveEntry entry = archive.CreateEntry(entryName);
+        }
+
+        DirectoryInfo destination = Directory.CreateDirectory(Path.Combine(GetTestFilePath(), "Bar"));
+        source.Position = 0;
+        Assert.Throws<IOException>(() => ZipFile.ExtractToDirectory(source, destination.FullName));
+    }
+
+    /// <summary>
+    /// This test ensures that a zipfile with path names that are invalid to this OS will throw errors
+    /// when an attempt is made to extract them.
+    /// </summary>
+    [Theory]
+    [InlineData("NullCharFileName_FromWindows")]
+    [InlineData("NullCharFileName_FromUnix")]
+    [PlatformSpecific(TestPlatforms.AnyUnix)]  // Checks Unix-specific invalid file path
+    public void Unix_ZipWithInvalidFileNames(string zipName)
+    {
+        string testDirectory = GetTestFilePath();
+        using Stream source = File.OpenRead(compat(zipName) + ".zip");
+        ZipFile.ExtractToDirectory(source, testDirectory);
+
+        Assert.True(File.Exists(Path.Combine(testDirectory, "a_6b6d")));
+    }
+
+    [Theory]
+    [InlineData("backslashes_FromUnix", "aa\\bb\\cc\\dd")]
+    [InlineData("backslashes_FromWindows", "aa\\bb\\cc\\dd")]
+    [InlineData("WindowsInvalid_FromUnix", "aa<b>d")]
+    [InlineData("WindowsInvalid_FromWindows", "aa<b>d")]
+    [PlatformSpecific(TestPlatforms.AnyUnix)]  // Checks Unix-specific invalid file path
+    public void Unix_ZipWithOSSpecificFileNames(string zipName, string fileName)
+    {
+        string tempDir = GetTestFilePath();
+        using Stream source = File.OpenRead(compat(zipName) + ".zip");
+        ZipFile.ExtractToDirectory(source, tempDir);
+        string[] results = Directory.GetFiles(tempDir, "*", SearchOption.AllDirectories);
+        Assert.Equal(1, results.Length);
+        Assert.Equal(fileName, Path.GetFileName(results[0]));
+    }
+
+    /// <summary>
+    /// This test checks whether or not ZipFile.ExtractToDirectory() is capable of handling filenames
+               /// which contain invalid path characters in Windows.
+    ///  Archive:  InvalidWindowsFileNameChars.zip
+    ///  Test/
+    ///  Test/normalText.txt
+    ///  Test"<>|^A^B^C^D^E^F^G^H^I^J^K^L^M^N^O^P^Q^R^S^T^U^V^W^X^Y^Z^[^\^]^^^_/
+    ///  Test"<>|^A^B^C^D^E^F^G^H^I^J^K^L^M^N^O^P^Q^R^S^T^U^V^W^X^Y^Z^[^\^]^^^_/TestText1"<>|^A^B^C^D^E^F^G^H^I^J^K^L^M^N^O^P^Q^R^S^T^U^V^W^X^Y^Z^[^\^]^^^_.txt
+    ///  TestEmpty/
+    ///  TestText"<>|^A^B^C^D^E^F^G^H^I^J^K^L^M^N^O^P^Q^R^S^T^U^V^W^X^Y^Z^[^\^]^^^_.txt
+    /// </summary>
+    [Theory]
+    [PlatformSpecific(TestPlatforms.Windows)]
+    [InlineData("InvalidWindowsFileNameChars.zip",  new string[] { "TestText______________________________________.txt" , "Test______________________________________/TestText1______________________________________.txt" , "Test/normalText.txt" })]
+    [InlineData("NullCharFileName_FromWindows.zip", new string[] { "a_6b6d" })]
+    [InlineData("NullCharFileName_FromUnix.zip",    new string[] { "a_6b6d" })]
+    [InlineData("WindowsInvalid_FromUnix.zip",      new string[] { "aa_b_d" })]
+    [InlineData("WindowsInvalid_FromWindows.zip",   new string[] { "aa_b_d" })]
+    public void Windows_ZipWithInvalidFileNames(string zipFileName, string[] expectedFiles)
+    {
+        string testDirectory = GetTestFilePath();
+
+        using Stream source = File.OpenRead(compat(zipFileName));
+        ZipFile.ExtractToDirectory(source, testDirectory);
+        foreach (string expectedFile in expectedFiles)
+        {
+            string path = Path.Combine(testDirectory, expectedFile);
+            Assert.True(File.Exists(path));
+            File.Delete(path);
+        }
+    }
+
+    [Theory]
+    [InlineData("backslashes_FromUnix", "dd")]
+    [InlineData("backslashes_FromWindows", "dd")]
+    [PlatformSpecific(TestPlatforms.Windows)]  // Checks Windows-specific invalid file path
+    public void Windows_ZipWithOSSpecificFileNames(string zipName, string fileName)
+    {
+        string tempDir = GetTestFilePath();
+        using Stream source = File.OpenRead(compat(zipName) + ".zip");
+        ZipFile.ExtractToDirectory(source, tempDir);
+        string[] results = Directory.GetFiles(tempDir, "*", SearchOption.AllDirectories);
+        Assert.Equal(1, results.Length);
+        Assert.Equal(fileName, Path.GetFileName(results[0]));
+    }
+
+    [Fact]
+    public void ExtractToDirectoryOverwrite()
+    {
+        string folderName = zfolder("normal");
+
+        using TempDirectory tempFolder = new(GetTestFilePath());
+        using Stream source = File.OpenRead(zfile("normal.zip"));
+        ZipFile.ExtractToDirectory(source, tempFolder.Path, overwriteFiles: false);
+        source.Position = 0;
+        Assert.Throws<IOException>(() => ZipFile.ExtractToDirectory(source, tempFolder.Path /* default false */));
+        source.Position = 0;
+        Assert.Throws<IOException>(() => ZipFile.ExtractToDirectory(source, tempFolder.Path, overwriteFiles: false));
+        source.Position = 0;
+        ZipFile.ExtractToDirectory(source, tempFolder.Path, overwriteFiles: true);
+
+        DirsEqual(tempFolder.Path, folderName);
+    }
+
+    [Fact]
+    public void ExtractToDirectoryOverwriteEncoding()
+    {
+        string folderName = zfolder("normal");
+
+        using TempDirectory tempFolder = new TempDirectory(GetTestFilePath());
+        using Stream source = File.OpenRead(zfile("normal.zip"));
+        ZipFile.ExtractToDirectory(source, tempFolder.Path, Encoding.UTF8, overwriteFiles: false);
+        source.Position = 0;
+        Assert.Throws<IOException>(() => ZipFile.ExtractToDirectory(source, tempFolder.Path, Encoding.UTF8 /* default false */));
+        source.Position = 0;
+        Assert.Throws<IOException>(() => ZipFile.ExtractToDirectory(source, tempFolder.Path, Encoding.UTF8, overwriteFiles: false));
+        source.Position = 0;
+        ZipFile.ExtractToDirectory(source, tempFolder.Path, Encoding.UTF8, overwriteFiles: true);
+
+        DirsEqual(tempFolder.Path, folderName);
+    }
+
+    [Fact]
+    public void FilesOutsideDirectory()
+    {
+        using MemoryStream source = new();
+        using (ZipArchive archive = new(source, ZipArchiveMode.Create, leaveOpen: true))
+        {
+            using (StreamWriter writer = new(archive.CreateEntry(Path.Combine("..", "entry1"), CompressionLevel.Optimal).Open()))
+            {
+                writer.Write("This is a test.");
+            }
+        }
+        source.Position = 0;
+        Assert.Throws<IOException>(() => ZipFile.ExtractToDirectory(source, GetTestFilePath()));
+    }
+
+    [Fact]
+    public void DirectoryEntryWithData()
+    {
+        using MemoryStream source = new();
+        using (ZipArchive archive = new(source, ZipArchiveMode.Create, leaveOpen: true))
+        {
+            using (StreamWriter writer = new(archive.CreateEntry("testdir" + Path.DirectorySeparatorChar, CompressionLevel.Optimal).Open()))
+            {
+                writer.Write("This is a test.");
+            }
+        }
+        source.Position = 0;
+        Assert.Throws<IOException>(() => ZipFile.ExtractToDirectory(source, GetTestFilePath()));
+    }
+
+    [Fact]
+    public void ExtractToDirectoryRoundTrip()
+    {
+        string folderName = zfolder("normal");
+        MemoryStream source = new();
+        using TempDirectory tempFolder = new();
+
+        ZipFile.CreateFromDirectory(folderName, source);
+        source.Position = 0;
+        ZipFile.ExtractToDirectory(source, tempFolder.Path, overwriteFiles: false);
+
+        DirFileNamesEqual(tempFolder.Path, folderName);
+    }
+}
index 30b94b7..950e301 100644 (file)
@@ -30,7 +30,7 @@ namespace System.IO.Compression.Tests
         [Fact]
         public void ExtractToDirectoryNull()
         {
-            AssertExtensions.Throws<ArgumentNullException>("sourceArchiveFileName", () => ZipFile.ExtractToDirectory(null, GetTestFilePath()));
+            AssertExtensions.Throws<ArgumentNullException>("sourceArchiveFileName", () => ZipFile.ExtractToDirectory(sourceArchiveFileName: null, GetTestFilePath()));
         }
 
         [Fact]
@@ -184,23 +184,27 @@ namespace System.IO.Compression.Tests
         }
 
         [Fact]
-        public void ExtractToDirectoryZipArchiveOverwrite()
+        public void FilesOutsideDirectory()
         {
-            string zipFileName = zfile("normal.zip");
-            string folderName = zfolder("normal");
+            string archivePath = GetTestFilePath();
+            using (ZipArchive archive = ZipFile.Open(archivePath, ZipArchiveMode.Create))
+            using (StreamWriter writer = new StreamWriter(archive.CreateEntry(Path.Combine("..", "entry1"), CompressionLevel.Optimal).Open()))
+            {
+                writer.Write("This is a test.");
+            }
+            Assert.Throws<IOException>(() => ZipFile.ExtractToDirectory(archivePath, GetTestFilePath()));
+        }
 
-            using (var tempFolder = new TempDirectory(GetTestFilePath()))
+        [Fact]
+        public void DirectoryEntryWithData()
+        {
+            string archivePath = GetTestFilePath();
+            using (ZipArchive archive = ZipFile.Open(archivePath, ZipArchiveMode.Create))
+            using (StreamWriter writer = new StreamWriter(archive.CreateEntry("testdir" + Path.DirectorySeparatorChar, CompressionLevel.Optimal).Open()))
             {
-                using (ZipArchive archive = ZipFile.Open(zipFileName, ZipArchiveMode.Read))
-                {
-                    archive.ExtractToDirectory(tempFolder.Path);
-                    Assert.Throws<IOException>(() => archive.ExtractToDirectory(tempFolder.Path /* default false */));
-                    Assert.Throws<IOException>(() => archive.ExtractToDirectory(tempFolder.Path, overwriteFiles: false));
-                    archive.ExtractToDirectory(tempFolder.Path, overwriteFiles: true);
-
-                    DirsEqual(tempFolder.Path, folderName);
-                }
+                writer.Write("This is a test.");
             }
+            Assert.Throws<IOException>(() => ZipFile.ExtractToDirectory(archivePath, GetTestFilePath()));
         }
     }
 }
diff --git a/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Open.cs b/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Open.cs
new file mode 100644 (file)
index 0000000..6738a83
--- /dev/null
@@ -0,0 +1,284 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.IO.Compression.Tests;
+
+public class ZipFile_Open : ZipFileTestBase
+{
+    [Fact]
+    public void InvalidConstructors()
+    {
+        //out of range enum values
+        Assert.Throws<ArgumentOutOfRangeException>(() => ZipFile.Open("bad file", (ZipArchiveMode)(10)));
+    }
+
+    [Fact]
+    public void InvalidFiles()
+    {
+        Assert.Throws<InvalidDataException>(() => ZipFile.OpenRead(bad("EOCDmissing.zip")));
+        using (TempFile testArchive = CreateTempCopyFile(bad("EOCDmissing.zip"), GetTestFilePath()))
+        {
+            Assert.Throws<InvalidDataException>(() => ZipFile.Open(testArchive.Path, ZipArchiveMode.Update));
+        }
+
+        Assert.Throws<InvalidDataException>(() => ZipFile.OpenRead(bad("CDoffsetOutOfBounds.zip")));
+        using (TempFile testArchive = CreateTempCopyFile(bad("CDoffsetOutOfBounds.zip"), GetTestFilePath()))
+        {
+            Assert.Throws<InvalidDataException>(() => ZipFile.Open(testArchive.Path, ZipArchiveMode.Update));
+        }
+
+        using (ZipArchive archive = ZipFile.OpenRead(bad("CDoffsetInBoundsWrong.zip")))
+        {
+            Assert.Throws<InvalidDataException>(() => { var x = archive.Entries; });
+        }
+
+        using (TempFile testArchive = CreateTempCopyFile(bad("CDoffsetInBoundsWrong.zip"), GetTestFilePath()))
+        {
+            Assert.Throws<InvalidDataException>(() => ZipFile.Open(testArchive.Path, ZipArchiveMode.Update));
+        }
+
+        using (ZipArchive archive = ZipFile.OpenRead(bad("numberOfEntriesDifferent.zip")))
+        {
+            Assert.Throws<InvalidDataException>(() => { var x = archive.Entries; });
+        }
+        using (TempFile testArchive = CreateTempCopyFile(bad("numberOfEntriesDifferent.zip"), GetTestFilePath()))
+        {
+            Assert.Throws<InvalidDataException>(() => ZipFile.Open(testArchive.Path, ZipArchiveMode.Update));
+        }
+
+        //read mode on empty file
+        using (var memoryStream = new MemoryStream())
+        {
+            Assert.Throws<InvalidDataException>(() => new ZipArchive(memoryStream));
+        }
+
+        //offset out of bounds
+        using (ZipArchive archive = ZipFile.OpenRead(bad("localFileOffsetOutOfBounds.zip")))
+        {
+            ZipArchiveEntry e = archive.Entries[0];
+            Assert.Throws<InvalidDataException>(() => e.Open());
+        }
+
+        using (TempFile testArchive = CreateTempCopyFile(bad("localFileOffsetOutOfBounds.zip"), GetTestFilePath()))
+        {
+            Assert.Throws<InvalidDataException>(() => ZipFile.Open(testArchive.Path, ZipArchiveMode.Update));
+        }
+
+        //compressed data offset + compressed size out of bounds
+        using (ZipArchive archive = ZipFile.OpenRead(bad("compressedSizeOutOfBounds.zip")))
+        {
+            ZipArchiveEntry e = archive.Entries[0];
+            Assert.Throws<InvalidDataException>(() => e.Open());
+        }
+
+        using (TempFile testArchive = CreateTempCopyFile(bad("compressedSizeOutOfBounds.zip"), GetTestFilePath()))
+        {
+            Assert.Throws<InvalidDataException>(() => ZipFile.Open(testArchive.Path, ZipArchiveMode.Update));
+        }
+
+        //signature wrong
+        using (ZipArchive archive = ZipFile.OpenRead(bad("localFileHeaderSignatureWrong.zip")))
+        {
+            ZipArchiveEntry e = archive.Entries[0];
+            Assert.Throws<InvalidDataException>(() => e.Open());
+        }
+
+        using (TempFile testArchive = CreateTempCopyFile(bad("localFileHeaderSignatureWrong.zip"), GetTestFilePath()))
+        {
+            Assert.Throws<InvalidDataException>(() => ZipFile.Open(testArchive.Path, ZipArchiveMode.Update));
+        }
+    }
+
+    [Fact]
+    public void InvalidInstanceMethods()
+    {
+        using (TempFile testArchive = CreateTempCopyFile(zfile("normal.zip"), GetTestFilePath()))
+        using (ZipArchive archive = ZipFile.Open(testArchive.Path, ZipArchiveMode.Update))
+        {
+            //non-existent entry
+            Assert.True(null == archive.GetEntry("nonExistentEntry"));
+            //null/empty string
+            Assert.Throws<ArgumentNullException>(() => archive.GetEntry(null));
+
+            ZipArchiveEntry entry = archive.GetEntry("first.txt");
+
+            //null/empty string
+            AssertExtensions.Throws<ArgumentException>("entryName", () => archive.CreateEntry(""));
+            Assert.Throws<ArgumentNullException>(() => archive.CreateEntry(null));
+        }
+    }
+
+    [Theory]
+    [InlineData("LZMA.zip", true)]
+    [InlineData("invalidDeflate.zip", false)]
+    public void UnsupportedCompressionRoutine(string zipName, bool throwsOnOpen)
+    {
+        string filename = bad(zipName);
+        using (ZipArchive archive = ZipFile.OpenRead(filename))
+        {
+            ZipArchiveEntry e = archive.Entries[0];
+            if (throwsOnOpen)
+            {
+                Assert.Throws<InvalidDataException>(() => e.Open());
+            }
+            else
+            {
+                using (Stream s = e.Open())
+                {
+                    Assert.Throws<InvalidDataException>(() => s.ReadByte());
+                }
+            }
+        }
+
+        using (TempFile updatedCopy = CreateTempCopyFile(filename, GetTestFilePath()))
+        {
+            string name;
+            long length, compressedLength;
+            DateTimeOffset lastWriteTime;
+            using (ZipArchive archive = ZipFile.Open(updatedCopy.Path, ZipArchiveMode.Update))
+            {
+                ZipArchiveEntry e = archive.Entries[0];
+                name = e.FullName;
+                lastWriteTime = e.LastWriteTime;
+                length = e.Length;
+                compressedLength = e.CompressedLength;
+                Assert.Throws<InvalidDataException>(() => e.Open());
+            }
+
+            //make sure that update mode preserves that unreadable file
+            using (ZipArchive archive = ZipFile.Open(updatedCopy.Path, ZipArchiveMode.Update))
+            {
+                ZipArchiveEntry e = archive.Entries[0];
+                Assert.Equal(name, e.FullName);
+                Assert.Equal(lastWriteTime, e.LastWriteTime);
+                Assert.Equal(length, e.Length);
+                Assert.Equal(compressedLength, e.CompressedLength);
+                Assert.Throws<InvalidDataException>(() => e.Open());
+            }
+        }
+    }
+
+    [Fact]
+    public void InvalidDates()
+    {
+        using (ZipArchive archive = ZipFile.OpenRead(bad("invaliddate.zip")))
+        {
+            Assert.Equal(new DateTime(1980, 1, 1, 0, 0, 0), archive.Entries[0].LastWriteTime.DateTime);
+        }
+
+        // Browser VFS does not support saving file attributes, so skip
+        if (!PlatformDetection.IsBrowser)
+        {
+            FileInfo fileWithBadDate = new FileInfo(GetTestFilePath());
+            fileWithBadDate.Create().Dispose();
+            fileWithBadDate.LastWriteTimeUtc = new DateTime(1970, 1, 1, 1, 1, 1);
+            string archivePath = GetTestFilePath();
+            using (FileStream output = File.Open(archivePath, FileMode.Create))
+            using (ZipArchive archive = new ZipArchive(output, ZipArchiveMode.Create))
+            {
+                archive.CreateEntryFromFile(fileWithBadDate.FullName, "SomeEntryName");
+            }
+            using (ZipArchive archive = ZipFile.OpenRead(archivePath))
+            {
+                Assert.Equal(new DateTime(1980, 1, 1, 0, 0, 0), archive.Entries[0].LastWriteTime.DateTime);
+            }
+        }
+    }
+
+    [Fact]
+    public void ReadStreamOps()
+    {
+        using (ZipArchive archive = ZipFile.OpenRead(zfile("normal.zip")))
+        {
+            foreach (ZipArchiveEntry e in archive.Entries)
+            {
+                using (Stream s = e.Open())
+                {
+                    Assert.True(s.CanRead, "Can read to read archive");
+                    Assert.False(s.CanWrite, "Can't write to read archive");
+                    Assert.False(s.CanSeek, "Can't seek on archive");
+                    Assert.Equal(LengthOfUnseekableStream(s), e.Length);
+                }
+            }
+        }
+    }
+
+    [Fact]
+    public void UpdateReadTwice()
+    {
+        using (TempFile testArchive = CreateTempCopyFile(zfile("small.zip"), GetTestFilePath()))
+        using (ZipArchive archive = ZipFile.Open(testArchive.Path, ZipArchiveMode.Update))
+        {
+            ZipArchiveEntry entry = archive.Entries[0];
+            string contents1, contents2;
+            using (StreamReader s = new StreamReader(entry.Open()))
+            {
+                contents1 = s.ReadToEnd();
+            }
+            using (StreamReader s = new StreamReader(entry.Open()))
+            {
+                contents2 = s.ReadToEnd();
+            }
+            Assert.Equal(contents1, contents2);
+        }
+    }
+
+    [Fact]
+    public async Task UpdateAddFile()
+    {
+        //add file
+        using (TempFile testArchive = CreateTempCopyFile(zfile("normal.zip"), GetTestFilePath()))
+        {
+            using (ZipArchive archive = ZipFile.Open(testArchive.Path, ZipArchiveMode.Update))
+            {
+                await UpdateArchive(archive, zmodified(Path.Combine("addFile", "added.txt")), "added.txt");
+            }
+            await IsZipSameAsDirAsync(testArchive.Path, zmodified("addFile"), ZipArchiveMode.Read);
+        }
+
+        //add file and read entries before
+        using (TempFile testArchive = CreateTempCopyFile(zfile("normal.zip"), GetTestFilePath()))
+        {
+            using (ZipArchive archive = ZipFile.Open(testArchive.Path, ZipArchiveMode.Update))
+            {
+                var x = archive.Entries;
+
+                await UpdateArchive(archive, zmodified(Path.Combine("addFile", "added.txt")), "added.txt");
+            }
+            await IsZipSameAsDirAsync(testArchive.Path, zmodified("addFile"), ZipArchiveMode.Read);
+        }
+
+        //add file and read entries after
+        using (TempFile testArchive = CreateTempCopyFile(zfile("normal.zip"), GetTestFilePath()))
+        {
+            using (ZipArchive archive = ZipFile.Open(testArchive.Path, ZipArchiveMode.Update))
+            {
+                await UpdateArchive(archive, zmodified(Path.Combine("addFile", "added.txt")), "added.txt");
+
+                var x = archive.Entries;
+            }
+            await IsZipSameAsDirAsync(testArchive.Path, zmodified("addFile"), ZipArchiveMode.Read);
+        }
+    }
+
+    private static async Task UpdateArchive(ZipArchive archive, string installFile, string entryName)
+    {
+        string fileName = installFile;
+        ZipArchiveEntry e = archive.CreateEntry(entryName);
+
+        var file = FileData.GetFile(fileName);
+        e.LastWriteTime = file.LastModifiedDate;
+
+        using (var stream = await StreamHelpers.CreateTempCopyStream(fileName))
+        {
+            using (Stream es = e.Open())
+            {
+                es.SetLength(0);
+                stream.CopyTo(es);
+            }
+        }
+    }
+}