using ICSharpCode.SharpZipLib.Core; using ICSharpCode.SharpZipLib.Zip.Compression; using System; using System.IO; using static ICSharpCode.SharpZipLib.Zip.Compression.Deflater; using static ICSharpCode.SharpZipLib.Zip.ZipEntryFactory; namespace ICSharpCode.SharpZipLib.Zip { /// /// FastZipEvents supports all events applicable to FastZip operations. /// public class FastZipEvents { /// /// Delegate to invoke when processing directories. /// public event EventHandler ProcessDirectory; /// /// Delegate to invoke when processing files. /// public ProcessFileHandler ProcessFile; /// /// Delegate to invoke during processing of files. /// public ProgressHandler Progress; /// /// Delegate to invoke when processing for a file has been completed. /// public CompletedFileHandler CompletedFile; /// /// Delegate to invoke when processing directory failures. /// public DirectoryFailureHandler DirectoryFailure; /// /// Delegate to invoke when processing file failures. /// public FileFailureHandler FileFailure; /// /// Raise the directory failure event. /// /// The directory causing the failure. /// The exception for this event. /// A boolean indicating if execution should continue or not. public bool OnDirectoryFailure(string directory, Exception e) { bool result = false; DirectoryFailureHandler handler = DirectoryFailure; if (handler != null) { var args = new ScanFailureEventArgs(directory, e); handler(this, args); result = args.ContinueRunning; } return result; } /// /// Fires the file failure handler delegate. /// /// The file causing the failure. /// The exception for this failure. /// A boolean indicating if execution should continue or not. public bool OnFileFailure(string file, Exception e) { FileFailureHandler handler = FileFailure; bool result = (handler != null); if (result) { var args = new ScanFailureEventArgs(file, e); handler(this, args); result = args.ContinueRunning; } return result; } /// /// Fires the ProcessFile delegate. /// /// The file being processed. /// A boolean indicating if execution should continue or not. public bool OnProcessFile(string file) { bool result = true; ProcessFileHandler handler = ProcessFile; if (handler != null) { var args = new ScanEventArgs(file); handler(this, args); result = args.ContinueRunning; } return result; } /// /// Fires the delegate /// /// The file whose processing has been completed. /// A boolean indicating if execution should continue or not. public bool OnCompletedFile(string file) { bool result = true; CompletedFileHandler handler = CompletedFile; if (handler != null) { var args = new ScanEventArgs(file); handler(this, args); result = args.ContinueRunning; } return result; } /// /// Fires the process directory delegate. /// /// The directory being processed. /// Flag indicating if the directory has matching files as determined by the current filter. /// A of true if the operation should continue; false otherwise. public bool OnProcessDirectory(string directory, bool hasMatchingFiles) { bool result = true; EventHandler handler = ProcessDirectory; if (handler != null) { var args = new DirectoryEventArgs(directory, hasMatchingFiles); handler(this, args); result = args.ContinueRunning; } return result; } /// /// The minimum timespan between events. /// /// The minimum period of time between events. /// /// The default interval is three seconds. public TimeSpan ProgressInterval { get { return progressInterval_; } set { progressInterval_ = value; } } #region Instance Fields private TimeSpan progressInterval_ = TimeSpan.FromSeconds(3); #endregion Instance Fields } /// /// FastZip provides facilities for creating and extracting zip files. /// public class FastZip { #region Enumerations /// /// Defines the desired handling when overwriting files during extraction. /// public enum Overwrite { /// /// Prompt the user to confirm overwriting /// Prompt, /// /// Never overwrite files. /// Never, /// /// Always overwrite files. /// Always } #endregion Enumerations #region Constructors /// /// Initialise a default instance of . /// public FastZip() { } /// /// Initialise a new instance of using the specified /// /// The time setting to use when creating or extracting Zip entries. /// Using TimeSetting.LastAccessTime[Utc] when /// creating an archive will set the file time to the moment of reading. /// public FastZip(TimeSetting timeSetting) { entryFactory_ = new ZipEntryFactory(timeSetting); restoreDateTimeOnExtract_ = true; } /// /// Initialise a new instance of using the specified /// /// The time to set all values for created or extracted Zip Entries. public FastZip(DateTime time) { entryFactory_ = new ZipEntryFactory(time); restoreDateTimeOnExtract_ = true; } /// /// Initialise a new instance of /// /// The events to use during operations. public FastZip(FastZipEvents events) { events_ = events; } #endregion Constructors #region Properties /// /// Get/set a value indicating whether empty directories should be created. /// public bool CreateEmptyDirectories { get { return createEmptyDirectories_; } set { createEmptyDirectories_ = value; } } /// /// Get / set the password value. /// public string Password { get { return password_; } set { password_ = value; } } /// /// Get / set the method of encrypting entries. /// /// /// Only applies when is set. /// Defaults to ZipCrypto for backwards compatibility purposes. /// public ZipEncryptionMethod EntryEncryptionMethod { get; set; } = ZipEncryptionMethod.ZipCrypto; /// /// Get or set the active when creating Zip files. /// /// public INameTransform NameTransform { get { return entryFactory_.NameTransform; } set { entryFactory_.NameTransform = value; } } /// /// Get or set the active when creating Zip files. /// public IEntryFactory EntryFactory { get { return entryFactory_; } set { if (value == null) { entryFactory_ = new ZipEntryFactory(); } else { entryFactory_ = value; } } } /// /// Gets or sets the setting for Zip64 handling when writing. /// /// /// The default value is dynamic which is not backwards compatible with old /// programs and can cause problems with XP's built in compression which cant /// read Zip64 archives. However it does avoid the situation were a large file /// is added and cannot be completed correctly. /// NOTE: Setting the size for entries before they are added is the best solution! /// By default the EntryFactory used by FastZip will set the file size. /// public UseZip64 UseZip64 { get { return useZip64_; } set { useZip64_ = value; } } /// /// Get/set a value indicating whether file dates and times should /// be restored when extracting files from an archive. /// /// The default value is false. public bool RestoreDateTimeOnExtract { get { return restoreDateTimeOnExtract_; } set { restoreDateTimeOnExtract_ = value; } } /// /// Get/set a value indicating whether file attributes should /// be restored during extract operations /// public bool RestoreAttributesOnExtract { get { return restoreAttributesOnExtract_; } set { restoreAttributesOnExtract_ = value; } } /// /// Get/set the Compression Level that will be used /// when creating the zip /// public Deflater.CompressionLevel CompressionLevel { get { return compressionLevel_; } set { compressionLevel_ = value; } } #endregion Properties #region Delegates /// /// Delegate called when confirming overwriting of files. /// public delegate bool ConfirmOverwriteDelegate(string fileName); #endregion Delegates #region CreateZip /// /// Create a zip file. /// /// The name of the zip file to create. /// The directory to source files from. /// True to recurse directories, false for no recursion. /// The file filter to apply. /// The directory filter to apply. public void CreateZip(string zipFileName, string sourceDirectory, bool recurse, string fileFilter, string directoryFilter) { CreateZip(File.Create(zipFileName), sourceDirectory, recurse, fileFilter, directoryFilter); } /// /// Create a zip file/archive. /// /// The name of the zip file to create. /// The directory to obtain files and directories from. /// True to recurse directories, false for no recursion. /// The file filter to apply. public void CreateZip(string zipFileName, string sourceDirectory, bool recurse, string fileFilter) { CreateZip(File.Create(zipFileName), sourceDirectory, recurse, fileFilter, null); } /// /// Create a zip archive sending output to the passed. /// /// The stream to write archive data to. /// The directory to source files from. /// True to recurse directories, false for no recursion. /// The file filter to apply. /// The directory filter to apply. /// The is closed after creation. public void CreateZip(Stream outputStream, string sourceDirectory, bool recurse, string fileFilter, string directoryFilter) { CreateZip(outputStream, sourceDirectory, recurse, fileFilter, directoryFilter, false); } /// /// Create a zip archive sending output to the passed. /// /// The stream to write archive data to. /// The directory to source files from. /// True to recurse directories, false for no recursion. /// The file filter to apply. /// The directory filter to apply. /// true to leave open after the zip has been created, false to dispose it. public void CreateZip(Stream outputStream, string sourceDirectory, bool recurse, string fileFilter, string directoryFilter, bool leaveOpen) { var scanner = new FileSystemScanner(fileFilter, directoryFilter); CreateZip(outputStream, sourceDirectory, recurse, scanner, leaveOpen); } /// /// Create a zip file. /// /// The name of the zip file to create. /// The directory to source files from. /// True to recurse directories, false for no recursion. /// The file filter to apply. /// The directory filter to apply. public void CreateZip(string zipFileName, string sourceDirectory, bool recurse, IScanFilter fileFilter, IScanFilter directoryFilter) { CreateZip(File.Create(zipFileName), sourceDirectory, recurse, fileFilter, directoryFilter, false); } /// /// Create a zip archive sending output to the passed. /// /// The stream to write archive data to. /// The directory to source files from. /// True to recurse directories, false for no recursion. /// The file filter to apply. /// The directory filter to apply. /// true to leave open after the zip has been created, false to dispose it. public void CreateZip(Stream outputStream, string sourceDirectory, bool recurse, IScanFilter fileFilter, IScanFilter directoryFilter, bool leaveOpen = false) { var scanner = new FileSystemScanner(fileFilter, directoryFilter); CreateZip(outputStream, sourceDirectory, recurse, scanner, leaveOpen); } /// /// Create a zip archive sending output to the passed. /// /// The stream to write archive data to. /// The directory to source files from. /// True to recurse directories, false for no recursion. /// For performing the actual file system scan /// true to leave open after the zip has been created, false to dispose it. /// The is closed after creation. private void CreateZip(Stream outputStream, string sourceDirectory, bool recurse, FileSystemScanner scanner, bool leaveOpen) { NameTransform = new ZipNameTransform(sourceDirectory); sourceDirectory_ = sourceDirectory; using (outputStream_ = new ZipOutputStream(outputStream)) { outputStream_.SetLevel((int)CompressionLevel); outputStream_.IsStreamOwner = !leaveOpen; outputStream_.NameTransform = null; // all required transforms handled by us if (false == string.IsNullOrEmpty(password_) && EntryEncryptionMethod != ZipEncryptionMethod.None) { outputStream_.Password = password_; } outputStream_.UseZip64 = UseZip64; scanner.ProcessFile += ProcessFile; if (this.CreateEmptyDirectories) { scanner.ProcessDirectory += ProcessDirectory; } if (events_ != null) { if (events_.FileFailure != null) { scanner.FileFailure += events_.FileFailure; } if (events_.DirectoryFailure != null) { scanner.DirectoryFailure += events_.DirectoryFailure; } } scanner.Scan(sourceDirectory, recurse); } } #endregion CreateZip #region ExtractZip /// /// Extract the contents of a zip file. /// /// The zip file to extract from. /// The directory to save extracted information in. /// A filter to apply to files. public void ExtractZip(string zipFileName, string targetDirectory, string fileFilter) { ExtractZip(zipFileName, targetDirectory, Overwrite.Always, null, fileFilter, null, restoreDateTimeOnExtract_); } /// /// Extract the contents of a zip file. /// /// The zip file to extract from. /// The directory to save extracted information in. /// The style of overwriting to apply. /// A delegate to invoke when confirming overwriting. /// A filter to apply to files. /// A filter to apply to directories. /// Flag indicating whether to restore the date and time for extracted files. /// Allow parent directory traversal in file paths (e.g. ../file) public void ExtractZip(string zipFileName, string targetDirectory, Overwrite overwrite, ConfirmOverwriteDelegate confirmDelegate, string fileFilter, string directoryFilter, bool restoreDateTime, bool allowParentTraversal = false) { Stream inputStream = File.Open(zipFileName, FileMode.Open, FileAccess.Read, FileShare.Read); ExtractZip(inputStream, targetDirectory, overwrite, confirmDelegate, fileFilter, directoryFilter, restoreDateTime, true, allowParentTraversal); } /// /// Extract the contents of a zip file held in a stream. /// /// The seekable input stream containing the zip to extract from. /// The directory to save extracted information in. /// The style of overwriting to apply. /// A delegate to invoke when confirming overwriting. /// A filter to apply to files. /// A filter to apply to directories. /// Flag indicating whether to restore the date and time for extracted files. /// Flag indicating whether the inputStream will be closed by this method. /// Allow parent directory traversal in file paths (e.g. ../file) public void ExtractZip(Stream inputStream, string targetDirectory, Overwrite overwrite, ConfirmOverwriteDelegate confirmDelegate, string fileFilter, string directoryFilter, bool restoreDateTime, bool isStreamOwner, bool allowParentTraversal = false) { if ((overwrite == Overwrite.Prompt) && (confirmDelegate == null)) { throw new ArgumentNullException(nameof(confirmDelegate)); } continueRunning_ = true; overwrite_ = overwrite; confirmDelegate_ = confirmDelegate; extractNameTransform_ = new WindowsNameTransform(targetDirectory, allowParentTraversal); fileFilter_ = new NameFilter(fileFilter); directoryFilter_ = new NameFilter(directoryFilter); restoreDateTimeOnExtract_ = restoreDateTime; using (zipFile_ = new ZipFile(inputStream, !isStreamOwner)) { if (password_ != null) { zipFile_.Password = password_; } System.Collections.IEnumerator enumerator = zipFile_.GetEnumerator(); while (continueRunning_ && enumerator.MoveNext()) { var entry = (ZipEntry)enumerator.Current; if (entry.IsFile) { // TODO Path.GetDirectory can fail here on invalid characters. if (directoryFilter_.IsMatch(Path.GetDirectoryName(entry.Name)) && fileFilter_.IsMatch(entry.Name)) { ExtractEntry(entry); } } else if (entry.IsDirectory) { if (directoryFilter_.IsMatch(entry.Name) && CreateEmptyDirectories) { ExtractEntry(entry); } } else { // Do nothing for volume labels etc... } } } } #endregion ExtractZip #region Internal Processing private void ProcessDirectory(object sender, DirectoryEventArgs e) { if (!e.HasMatchingFiles && CreateEmptyDirectories) { if (events_ != null) { events_.OnProcessDirectory(e.Name, e.HasMatchingFiles); } if (e.ContinueRunning) { if (e.Name != sourceDirectory_) { ZipEntry entry = entryFactory_.MakeDirectoryEntry(e.Name); outputStream_.PutNextEntry(entry); } } } } private void ProcessFile(object sender, ScanEventArgs e) { if ((events_ != null) && (events_.ProcessFile != null)) { events_.ProcessFile(sender, e); } if (e.ContinueRunning) { try { // The open below is equivalent to OpenRead which guarantees that if opened the // file will not be changed by subsequent openers, but precludes opening in some cases // were it could succeed. ie the open may fail as its already open for writing and the share mode should reflect that. using (FileStream stream = File.Open(e.Name, FileMode.Open, FileAccess.Read, FileShare.Read)) { ZipEntry entry = entryFactory_.MakeFileEntry(e.Name); // Set up AES encryption for the entry if required. ConfigureEntryEncryption(entry); outputStream_.PutNextEntry(entry); AddFileContents(e.Name, stream); } } catch (Exception ex) { if (events_ != null) { continueRunning_ = events_.OnFileFailure(e.Name, ex); } else { continueRunning_ = false; throw; } } } } // Set up the encryption method to use for the specific entry. private void ConfigureEntryEncryption(ZipEntry entry) { // Only alter the entries options if AES isn't already enabled for it // (it might have been set up by the entry factory, and if so we let that take precedence) if (!string.IsNullOrEmpty(Password) && entry.AESEncryptionStrength == 0) { switch (EntryEncryptionMethod) { case ZipEncryptionMethod.AES128: entry.AESKeySize = 128; break; case ZipEncryptionMethod.AES256: entry.AESKeySize = 256; break; } } } private void AddFileContents(string name, Stream stream) { if (stream == null) { throw new ArgumentNullException(nameof(stream)); } if (buffer_ == null) { buffer_ = new byte[4096]; } if ((events_ != null) && (events_.Progress != null)) { StreamUtils.Copy(stream, outputStream_, buffer_, events_.Progress, events_.ProgressInterval, this, name); } else { StreamUtils.Copy(stream, outputStream_, buffer_); } if (events_ != null) { continueRunning_ = events_.OnCompletedFile(name); } } private void ExtractFileEntry(ZipEntry entry, string targetName) { bool proceed = true; if (overwrite_ != Overwrite.Always) { if (File.Exists(targetName)) { if ((overwrite_ == Overwrite.Prompt) && (confirmDelegate_ != null)) { proceed = confirmDelegate_(targetName); } else { proceed = false; } } } if (proceed) { if (events_ != null) { continueRunning_ = events_.OnProcessFile(entry.Name); } if (continueRunning_) { try { using (FileStream outputStream = File.Create(targetName)) { if (buffer_ == null) { buffer_ = new byte[4096]; } using (var inputStream = zipFile_.GetInputStream(entry)) { if ((events_ != null) && (events_.Progress != null)) { StreamUtils.Copy(inputStream, outputStream, buffer_, events_.Progress, events_.ProgressInterval, this, entry.Name, entry.Size); } else { StreamUtils.Copy(inputStream, outputStream, buffer_); } } if (events_ != null) { continueRunning_ = events_.OnCompletedFile(entry.Name); } } if (restoreDateTimeOnExtract_) { switch (entryFactory_.Setting) { case TimeSetting.CreateTime: File.SetCreationTime(targetName, entry.DateTime); break; case TimeSetting.CreateTimeUtc: File.SetCreationTimeUtc(targetName, entry.DateTime); break; case TimeSetting.LastAccessTime: File.SetLastAccessTime(targetName, entry.DateTime); break; case TimeSetting.LastAccessTimeUtc: File.SetLastAccessTimeUtc(targetName, entry.DateTime); break; case TimeSetting.LastWriteTime: File.SetLastWriteTime(targetName, entry.DateTime); break; case TimeSetting.LastWriteTimeUtc: File.SetLastWriteTimeUtc(targetName, entry.DateTime); break; case TimeSetting.Fixed: File.SetLastWriteTime(targetName, entryFactory_.FixedDateTime); break; default: throw new ZipException("Unhandled time setting in ExtractFileEntry"); } } if (RestoreAttributesOnExtract && entry.IsDOSEntry && (entry.ExternalFileAttributes != -1)) { var fileAttributes = (FileAttributes)entry.ExternalFileAttributes; // TODO: FastZip - Setting of other file attributes on extraction is a little trickier. fileAttributes &= (FileAttributes.Archive | FileAttributes.Normal | FileAttributes.ReadOnly | FileAttributes.Hidden); File.SetAttributes(targetName, fileAttributes); } } catch (Exception ex) { if (events_ != null) { continueRunning_ = events_.OnFileFailure(targetName, ex); } else { continueRunning_ = false; throw; } } } } } private void ExtractEntry(ZipEntry entry) { bool doExtraction = entry.IsCompressionMethodSupported(); string targetName = entry.Name; if (doExtraction) { if (entry.IsFile) { targetName = extractNameTransform_.TransformFile(targetName); } else if (entry.IsDirectory) { targetName = extractNameTransform_.TransformDirectory(targetName); } doExtraction = !(string.IsNullOrEmpty(targetName)); } // TODO: Fire delegate/throw exception were compression method not supported, or name is invalid? string dirName = string.Empty; if (doExtraction) { if (entry.IsDirectory) { dirName = targetName; } else { dirName = Path.GetDirectoryName(Path.GetFullPath(targetName)); } } if (doExtraction && !Directory.Exists(dirName)) { if (!entry.IsDirectory || CreateEmptyDirectories) { try { continueRunning_ = events_?.OnProcessDirectory(dirName, true) ?? true; if (continueRunning_) { Directory.CreateDirectory(dirName); if (entry.IsDirectory && restoreDateTimeOnExtract_) { switch (entryFactory_.Setting) { case TimeSetting.CreateTime: Directory.SetCreationTime(dirName, entry.DateTime); break; case TimeSetting.CreateTimeUtc: Directory.SetCreationTimeUtc(dirName, entry.DateTime); break; case TimeSetting.LastAccessTime: Directory.SetLastAccessTime(dirName, entry.DateTime); break; case TimeSetting.LastAccessTimeUtc: Directory.SetLastAccessTimeUtc(dirName, entry.DateTime); break; case TimeSetting.LastWriteTime: Directory.SetLastWriteTime(dirName, entry.DateTime); break; case TimeSetting.LastWriteTimeUtc: Directory.SetLastWriteTimeUtc(dirName, entry.DateTime); break; case TimeSetting.Fixed: Directory.SetLastWriteTime(dirName, entryFactory_.FixedDateTime); break; default: throw new ZipException("Unhandled time setting in ExtractEntry"); } } } else { doExtraction = false; } } catch (Exception ex) { doExtraction = false; if (events_ != null) { if (entry.IsDirectory) { continueRunning_ = events_.OnDirectoryFailure(targetName, ex); } else { continueRunning_ = events_.OnFileFailure(targetName, ex); } } else { continueRunning_ = false; throw; } } } } if (doExtraction && entry.IsFile) { ExtractFileEntry(entry, targetName); } } private static int MakeExternalAttributes(FileInfo info) { return (int)info.Attributes; } private static bool NameIsValid(string name) { return !string.IsNullOrEmpty(name) && (name.IndexOfAny(Path.GetInvalidPathChars()) < 0); } #endregion Internal Processing #region Instance Fields private bool continueRunning_; private byte[] buffer_; private ZipOutputStream outputStream_; private ZipFile zipFile_; private string sourceDirectory_; private NameFilter fileFilter_; private NameFilter directoryFilter_; private Overwrite overwrite_; private ConfirmOverwriteDelegate confirmDelegate_; private bool restoreDateTimeOnExtract_; private bool restoreAttributesOnExtract_; private bool createEmptyDirectories_; private FastZipEvents events_; private IEntryFactory entryFactory_ = new ZipEntryFactory(); private INameTransform extractNameTransform_; private UseZip64 useZip64_ = UseZip64.Dynamic; private CompressionLevel compressionLevel_ = CompressionLevel.DEFAULT_COMPRESSION; private string password_; #endregion Instance Fields } }