// Copyright (C) 2025, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. #nullable enable using System; using System.Collections.Generic; using System.IO; using Duplicati.Library.Interface; using Duplicati.Library.Logging; using SharpCompress.Common; namespace Duplicati.Library.Compression.ZipCompression { /// /// An abstraction of a ZIP archive as a FileArchive, based on either SharpCompress or System.IO.Compression. /// Please note, duplicati does not require both Read & Write access at the same time so this has not been implemented. /// public class FileArchiveZip : ICompression { /// /// The compression type used by the ZIP archive /// public enum CompressionLibrary { /// /// Automatically select the best library /// Auto = 0, /// /// Use the SharpCompress library /// SharpCompress = 1, /// /// Use the built-in library /// BuiltIn = 2 } /// /// The tag used for logging /// private static readonly string LOGTAG = Logging.Log.LogTagFromType(); /// /// The commandline option for toggling the compression level /// private const string COMPRESSION_LEVEL_OPTION = "zip-compression-level"; /// /// The old commandline option for toggling the compression level /// private const string COMPRESSION_LEVEL_OPTION_ALIAS = "compression-level"; /// /// The commandline option for toggling the compression method /// private const string COMPRESSION_METHOD_OPTION = "zip-compression-method"; /// /// The commandline option for toggling the compression library /// private const string COMPRESSION_LIBRARY_OPTION = "zip-compression-library"; /// /// The default compression level /// private const SharpCompress.Compressors.Deflate.CompressionLevel DEFAULT_COMPRESSION_LEVEL = SharpCompress.Compressors.Deflate.CompressionLevel.Level9; /// /// The default compression method /// private const CompressionType DEFAULT_COMPRESSION_METHOD = CompressionType.Deflate; /// /// The archive to use /// private readonly IZipArchive m_archive; /// /// The fallback archive to use, if any /// private IZipArchive? m_fallbackArchive = null; /// /// The parameters used to create the archive /// private readonly (Stream Stream, ParsedZipOptions Options, CompressionLibrary Library) m_creationParams; /// /// Default constructor, used to read file extension and supported commands /// public FileArchiveZip() { m_archive = null!; } /// /// Constructs a new Zip instance. /// Access mode is specified by mode parameter. /// Note that stream would not be disposed by FileArchiveZip instance so /// you may reuse it and have to dispose it yourself. /// /// The stream to read or write depending access mode /// The archive access mode /// The options passed on the commandline public FileArchiveZip(Stream stream, ArchiveMode mode, IDictionary options) { var compressionType = DEFAULT_COMPRESSION_METHOD; var compressionLevel = DEFAULT_COMPRESSION_LEVEL; CompressionType tmptype; if (options.TryGetValue(COMPRESSION_METHOD_OPTION, out var cpmethod) && Enum.TryParse(cpmethod, true, out tmptype)) compressionType = tmptype; if (options.TryGetValue(COMPRESSION_LEVEL_OPTION, out var cplvl) && int.TryParse(cplvl, out var tmplvl)) compressionLevel = (SharpCompress.Compressors.Deflate.CompressionLevel)Math.Max(Math.Min(9, tmplvl), 0); else if (options.TryGetValue(COMPRESSION_LEVEL_OPTION_ALIAS, out cplvl) && int.TryParse(cplvl, out tmplvl)) compressionLevel = (SharpCompress.Compressors.Deflate.CompressionLevel)Math.Max(Math.Min(9, tmplvl), 0); var compressionLibrary = CompressionLibrary.Auto; if (options.ContainsKey(COMPRESSION_LIBRARY_OPTION) && options.TryGetValue(COMPRESSION_LIBRARY_OPTION, out var cplib) && Enum.TryParse(cplib, true, out var tmpcplib)) compressionLibrary = tmpcplib; var unittestMode = Utility.Utility.ParseBoolOption(options.AsReadOnly(), "unittest-mode"); var parsedOptions = new ParsedZipOptions(compressionLevel, compressionType, unittestMode); var userCompressionLibrary = compressionLibrary; if (compressionLibrary == CompressionLibrary.Auto) { if (compressionType != CompressionType.Deflate) compressionLibrary = CompressionLibrary.SharpCompress; else compressionLibrary = CompressionLibrary.BuiltIn; } IZipArchive? archive = null; try { if (compressionLibrary == CompressionLibrary.BuiltIn) archive = new BuiltinZipArchive(stream, mode, parsedOptions); } catch (Exception ex) { if (mode == ArchiveMode.Write) throw; Log.WriteWarningMessage(LOGTAG, "system-io-compression-error", ex, "Failed to create built-in ZIP archive, falling back to SharpCompress"); } m_archive = archive ?? new SharpCompressZipArchive(stream, mode, parsedOptions); m_creationParams = (stream, parsedOptions, userCompressionLibrary); } #region IFileArchive Members /// /// Gets the filename extension used by the compression module /// public string FilenameExtension { get { return "zip"; } } /// /// Gets a friendly name for the compression module /// public string DisplayName { get { return Strings.FileArchiveZip.DisplayName; } } /// /// Gets a description of the compression module /// public string Description { get { return Strings.FileArchiveZip.Description; } } /// /// Gets a list of commands supported by the compression module /// public IList SupportedCommands { get { // Various compression methods to use var methods = new[] { CompressionType.None.ToString(), CompressionType.Deflate.ToString(), CompressionType.BZip2.ToString(), CompressionType.LZMA.ToString(), CompressionType.PPMd.ToString(), CompressionType.GZip.ToString(), CompressionType.Xz.ToString(), CompressionType.Deflate64.ToString(), }; return new List([ new CommandLineArgument(COMPRESSION_LEVEL_OPTION, CommandLineArgument.ArgumentType.Enumeration, Strings.FileArchiveZip.CompressionlevelShort, Strings.FileArchiveZip.CompressionlevelLong, DEFAULT_COMPRESSION_LEVEL.ToString(), null, new string[] {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}), new CommandLineArgument(COMPRESSION_LEVEL_OPTION_ALIAS, CommandLineArgument.ArgumentType.Enumeration, Strings.FileArchiveZip.CompressionlevelShort, Strings.FileArchiveZip.CompressionlevelLong, DEFAULT_COMPRESSION_LEVEL.ToString(), null, new string[] {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}, Strings.FileArchiveZip.CompressionlevelDeprecated(COMPRESSION_LEVEL_OPTION)), new CommandLineArgument(COMPRESSION_METHOD_OPTION, CommandLineArgument.ArgumentType.Enumeration, Strings.FileArchiveZip.CompressionmethodShort, Strings.FileArchiveZip.CompressionmethodLong(COMPRESSION_LEVEL_OPTION), DEFAULT_COMPRESSION_METHOD.ToString(), null, methods), new CommandLineArgument("zip-compression-zip64", CommandLineArgument.ArgumentType.Boolean, Strings.FileArchiveZip.Compressionzip64Short, Strings.FileArchiveZip.Compressionzip64Long, "true", null, null, Strings.FileArchiveZip.Compressionzip64Deprecated), new CommandLineArgument(COMPRESSION_LIBRARY_OPTION, CommandLineArgument.ArgumentType.Enumeration, Strings.FileArchiveZip.CompressionlibraryShort, Strings.FileArchiveZip.CompressionlibraryLong, CompressionLibrary.Auto.ToString(), null, Enum.GetNames(typeof(CompressionLibrary))) ]); } } public long Size => m_archive.Size; public long FlushBufferSize => m_archive.FlushBufferSize; #endregion public void Dispose() { if (m_fallbackArchive == null) m_archive.Dispose(); m_fallbackArchive?.Dispose(); } public string[] ListFiles(string prefix) => m_archive.ListFiles(prefix); public IEnumerable> ListFilesWithSize(string prefix) => m_archive.ListFilesWithSize(prefix); public Stream? OpenRead(string file) { // If we have started the fallback archive, we should continue using it if (m_fallbackArchive != null) return m_fallbackArchive.OpenRead(file); try { return m_archive.OpenRead(file); } catch (Exception ex) { if (m_creationParams.Library == CompressionLibrary.Auto && m_archive is not SharpCompressZipArchive && m_creationParams.Stream.CanSeek) { Log.WriteWarningMessage(LOGTAG, "CompressionReadErrorFallback", ex, "Failed to open file with built-in ZIP archive, falling back to SharpCompress"); try { m_creationParams.Item1.Seek(0, SeekOrigin.Begin); m_archive.Dispose(); } catch { } m_fallbackArchive = new SharpCompressZipArchive(m_creationParams.Stream, ArchiveMode.Read, m_creationParams.Options); return m_fallbackArchive.OpenRead(file); } throw; } } public DateTime GetLastWriteTime(string file) => m_archive.GetLastWriteTime(file); public bool FileExists(string file) => m_archive.FileExists(file); public Stream CreateFile(string file, CompressionHint hint, DateTime lastWrite) => m_archive.CreateFile(file, hint, lastWrite); } }