#if UNMANAGED using System.Runtime.InteropServices; namespace SevenZip; public record CompressStreamEntry(Stream? Stream, DateTimeOffset? CreateTime, DateTimeOffset? LastWrite, DateTimeOffset? LastAccess) { public CompressStreamEntry(Stream? Stream, DateTimeOffset? fileTime) : this(Stream, fileTime, fileTime, fileTime) { } } internal sealed class ArchiveUpdateCallbackWithFileTime : CallbackBase, IArchiveUpdateCallback, ICryptoGetTextPassword2, IDisposable { #region Fields /// /// _files.Count if do not count directories /// private int _actualFilesCount; /// /// For Compressing event. /// private long _bytesCount; private long _bytesWritten; private long _bytesWrittenOld; private SevenZipCompressor _compressor; /// /// No directories. /// private bool _directoryStructure; /// /// Rate of the done work from [0, 1] /// private float _doneRate; /// /// The names of the archive entries /// private string[] _entries; /// /// Array of files to pack /// private FileInfo[]? _files; private InStreamWrapper _fileStream; private uint _indexInArchive; private uint _indexOffset; /// /// Common root of file names length. /// private int _rootLength; /// /// Input streams to be compressed. /// private CompressStreamEntry[] _streamEntries; private UpdateData _updateData; private List _wrappersToDispose; /// /// Gets or sets the default item name used in MemoryStream compression. /// public string DefaultItemName { private get; set; } /// /// Gets or sets the value indicating whether to compress as fast as possible, without calling events. /// public bool FastCompression { private get; set; } private int _memoryPressure; #endregion #region Constructors /// /// Initializes a new instance of the ArchiveUpdateCallback class /// /// Array of files to pack /// Common file names root length /// The owner of the callback /// The compression parameters. /// Preserve directory structure. public ArchiveUpdateCallbackWithFileTime( FileInfo[] files, int rootLength, SevenZipCompressor compressor, UpdateData updateData, bool directoryStructure) { Init(files, rootLength, compressor, updateData, directoryStructure); } /// /// Initializes a new instance of the ArchiveUpdateCallback class /// /// Array of files to pack /// Common file names root length /// The archive password /// The owner of the callback /// The compression parameters. /// Preserve directory structure. public ArchiveUpdateCallbackWithFileTime( FileInfo[] files, int rootLength, string password, SevenZipCompressor compressor, UpdateData updateData, bool directoryStructure) : base(password) { Init(files, rootLength, compressor, updateData, directoryStructure); } /// /// Initializes a new instance of the ArchiveUpdateCallback class /// /// The input stream /// The owner of the callback /// The compression parameters. /// Preserve directory structure. public ArchiveUpdateCallbackWithFileTime( Stream stream, SevenZipCompressor compressor, UpdateData updateData, bool directoryStructure) { Init(stream, compressor, updateData, directoryStructure); } /// /// Initializes a new instance of the ArchiveUpdateCallback class /// /// The input stream /// The archive password /// The owner of the callback /// The compression parameters. /// Preserve directory structure. public ArchiveUpdateCallbackWithFileTime( Stream stream, string password, SevenZipCompressor compressor, UpdateData updateData, bool directoryStructure) : base(password) { Init(stream, compressor, updateData, directoryStructure); } /// /// Initializes a new instance of the ArchiveUpdateCallback class /// /// Dictionary<file stream, name of the archive entry> /// The owner of the callback /// The compression parameters. /// Preserve directory structure. public ArchiveUpdateCallbackWithFileTime( IDictionary streamDict, SevenZipCompressor compressor, UpdateData updateData, bool directoryStructure) { Init(streamDict, compressor, updateData, directoryStructure); } /// /// Initializes a new instance of the ArchiveUpdateCallback class /// /// Dictionary<file stream, name of the archive entry> /// The archive password /// The owner of the callback /// The compression parameters. /// Preserve directory structure. public ArchiveUpdateCallbackWithFileTime( IDictionary streamDict, string password, SevenZipCompressor compressor, UpdateData updateData, bool directoryStructure) : base(password) { Init(streamDict, compressor, updateData, directoryStructure); } private void CommonInit(SevenZipCompressor compressor, UpdateData updateData, bool directoryStructure) { _compressor = compressor; _indexInArchive = updateData.FilesCount; _indexOffset = updateData.Mode != InternalCompressionMode.Append ? 0 : _indexInArchive; if (_compressor.ArchiveFormat == OutArchiveFormat.Zip) { _wrappersToDispose = new List(); } _updateData = updateData; _directoryStructure = directoryStructure; DefaultItemName = "default"; } private void Init( FileInfo[] files, int rootLength, SevenZipCompressor compressor, UpdateData updateData, bool directoryStructure) { _files = files; _rootLength = rootLength; if (files != null) { foreach (var fi in files) { if (fi.Exists) { _bytesCount += fi.Length; if ((fi.Attributes & FileAttributes.Directory) == 0) { _actualFilesCount++; } } } } CommonInit(compressor, updateData, directoryStructure); } private void Init( Stream stream, SevenZipCompressor compressor, UpdateData updateData, bool directoryStructure) { _fileStream = new InStreamWrapper(stream, false); _fileStream.BytesRead += IntEventArgsHandler; _actualFilesCount = 1; try { _bytesCount = stream.Length; } catch (NotSupportedException) { _bytesCount = -1; } try { stream.Seek(0, SeekOrigin.Begin); } catch (NotSupportedException) { _bytesCount = -1; } CommonInit(compressor, updateData, directoryStructure); } private void Init( IDictionary streamDict, SevenZipCompressor compressor, UpdateData updateData, bool directoryStructure) { _streamEntries = new CompressStreamEntry[streamDict.Count]; streamDict.Values.ToArray().CopyTo(_streamEntries, 0); _entries = new string[streamDict.Count]; streamDict.Keys.CopyTo(_entries, 0); _actualFilesCount = streamDict.Count; foreach (var entry in _streamEntries) { if (entry.Stream != null) { _bytesCount += entry.Stream.Length; } } CommonInit(compressor, updateData, directoryStructure); } #endregion /// /// Gets or sets the dictionary size. /// public float DictionarySize { set { _memoryPressure = (int)(value * 1024 * 1024); GC.AddMemoryPressure(_memoryPressure); } } /// /// Raises events for the GetStream method. /// /// The current item index. /// True if not cancelled; otherwise, false. private bool EventsForGetStream(uint index) { if (!FastCompression) { if (_fileStream != null) { _fileStream.BytesRead += IntEventArgsHandler; } _doneRate += 1.0f / _actualFilesCount; var fiea = new FileNameEventArgs(_files != null ? _files[index].Name : _entries[index], PercentDoneEventArgs.ProducePercentDone(_doneRate)); OnFileCompression(fiea); if (fiea.Cancel) { Canceled = true; return false; } } return true; } #region Events /// /// Occurs when the next file is going to be packed. /// /// Occurs when 7-zip engine requests for an input stream for the next file to pack it public event EventHandler FileCompressionStarted; /// /// Occurs when data are being compressed. /// public event EventHandler Compressing; /// /// Occurs when the current file was compressed. /// public event EventHandler FileCompressionFinished; private void OnFileCompression(FileNameEventArgs e) { FileCompressionStarted?.Invoke(this, e); } private void OnCompressing(ProgressEventArgs e) { Compressing?.Invoke(this, e); } private void OnFileCompressionFinished(EventArgs e) { FileCompressionFinished?.Invoke(this, e); } #endregion #region IArchiveUpdateCallback Members public void SetTotal(ulong total) { } public void SetCompleted(ref ulong completeValue) { } public int GetUpdateItemInfo(uint index, ref int newData, ref int newProperties, ref uint indexInArchive) { switch (_updateData.Mode) { case InternalCompressionMode.Create: newData = 1; newProperties = 1; indexInArchive = uint.MaxValue; break; case InternalCompressionMode.Append: if (index < _indexInArchive) { newData = 0; newProperties = 0; indexInArchive = index; } else { newData = 1; newProperties = 1; indexInArchive = uint.MaxValue; } break; case InternalCompressionMode.Modify: newData = 0; newProperties = Convert.ToInt32(_updateData.FileNamesToModify.ContainsKey((int)index) && _updateData.FileNamesToModify[(int)index] != null); if (_updateData.FileNamesToModify.ContainsKey((int)index) && _updateData.FileNamesToModify[(int)index] == null) { indexInArchive = (uint)_updateData.ArchiveFileData.Count; foreach (var pairModification in _updateData.FileNamesToModify) { if (pairModification.Key <= index && (pairModification.Value == null)) { do { indexInArchive--; } while (indexInArchive > 0 && _updateData.FileNamesToModify.ContainsKey((int)indexInArchive) && _updateData.FileNamesToModify[(int)indexInArchive] == null); } } } else { indexInArchive = index; } break; } return 0; } public int GetProperty(uint index, ItemPropId propID, ref PropVariant value) { index -= _indexOffset; try { switch (propID) { case ItemPropId.IsAnti: value.VarType = VarEnum.VT_BOOL; value.UInt64Value = 0; break; case ItemPropId.Path: #region Path value.VarType = VarEnum.VT_BSTR; string val = DefaultItemName; if (_updateData.Mode != InternalCompressionMode.Modify) { if (_files == null) { if (_entries != null) { val = _entries[index]; } } else { if (_directoryStructure) { if (_rootLength > 0) { val = _files[index].FullName.Substring(_rootLength); } else { val = _files[index].FullName[0] + _files[index].FullName.Substring(2); } } else { val = _files[index].Name; } } } else { val = _updateData.FileNamesToModify[(int)index]; } value.Value = Marshal.StringToBSTR(val); #endregion break; case ItemPropId.IsDirectory: value.VarType = VarEnum.VT_BOOL; if (_updateData.Mode != InternalCompressionMode.Modify) { if (_files == null) { if (_streamEntries == null) { value.UInt64Value = 0; } else { value.UInt64Value = (ulong)(_streamEntries[index] == null ? 1 : 0); } } else { value.UInt64Value = (byte)(_files[index].Attributes & FileAttributes.Directory); } } else { value.UInt64Value = Convert.ToUInt64(_updateData.ArchiveFileData[(int)index].IsDirectory); } break; case ItemPropId.Size: #region Size value.VarType = VarEnum.VT_UI8; ulong size; if (_updateData.Mode != InternalCompressionMode.Modify) { if (_files == null) { if (_streamEntries == null) { size = _bytesCount > 0 ? (ulong)_bytesCount : 0; } else { size = (ulong)(_streamEntries[index].Stream == null ? 0 : _streamEntries[index].Stream.Length); } } else { size = (_files[index].Attributes & FileAttributes.Directory) == 0 ? (ulong)_files[index].Length : 0; } } else { size = _updateData.ArchiveFileData[(int)index].Size; } value.UInt64Value = size; #endregion break; case ItemPropId.Attributes: value.VarType = VarEnum.VT_UI4; if (_updateData.Mode != InternalCompressionMode.Modify) { if (_files == null) { if (_streamEntries == null) { value.UInt32Value = (uint)FileAttributes.Normal; } else { value.UInt32Value = (uint)(_streamEntries[index].Stream == null ? FileAttributes.Directory : FileAttributes.Normal); } } else { value.UInt32Value = (uint)_files[index].Attributes; } } else { value.UInt32Value = _updateData.ArchiveFileData[(int)index].Attributes; } break; #region Times case ItemPropId.CreationTime: value.VarType = VarEnum.VT_FILETIME; if (_updateData.Mode != InternalCompressionMode.Modify) { if (_files != null) value.Int64Value = _files[index].CreationTime.ToFileTime(); else if (_streamEntries != null && _streamEntries[index].CreateTime.HasValue) value.Int64Value = _streamEntries[index].CreateTime.Value.ToFileTime(); else value.Int64Value = DateTime.Now.ToFileTime(); } else { value.Int64Value = _updateData.ArchiveFileData[(int)index].CreationTime.ToFileTime(); } break; case ItemPropId.LastAccessTime: value.VarType = VarEnum.VT_FILETIME; if (_updateData.Mode != InternalCompressionMode.Modify) { if (_files != null) value.Int64Value = _files[index].LastAccessTime.ToFileTime(); else if (_streamEntries != null && _streamEntries[index].LastAccess.HasValue) value.Int64Value = _streamEntries[index].LastAccess.Value.ToFileTime(); else value.Int64Value = DateTime.Now.ToFileTime(); } else { value.Int64Value = _updateData.ArchiveFileData[(int)index].LastAccessTime.ToFileTime(); } break; case ItemPropId.LastWriteTime: value.VarType = VarEnum.VT_FILETIME; if (_updateData.Mode != InternalCompressionMode.Modify) { if (_files == null) value.Int64Value = DateTime.Now.ToFileTime(); else if (_streamEntries != null && _streamEntries[index].LastWrite.HasValue) value.Int64Value = _streamEntries[index].LastWrite.Value.ToFileTime(); else value.Int64Value = _files[index].LastWriteTime.ToFileTime(); } else { value.Int64Value = _updateData.ArchiveFileData[(int)index].LastWriteTime.ToFileTime(); } break; #endregion case ItemPropId.Extension: #region Extension value.VarType = VarEnum.VT_BSTR; if (_updateData.Mode != InternalCompressionMode.Modify) { try { val = _files != null ? _files[index].Extension.Substring(1) : _entries == null ? "" : Path.GetExtension(_entries[index]); value.Value = Marshal.StringToBSTR(val); } catch (ArgumentException) { value.Value = Marshal.StringToBSTR(""); } } else { val = Path.GetExtension(_updateData.ArchiveFileData[(int)index].FileName); value.Value = Marshal.StringToBSTR(val); } #endregion break; } } catch (Exception e) { AddException(e); } return 0; } /// /// Gets the stream for 7-zip library. /// /// File index /// Input file stream /// Zero if Ok public int GetStream(uint index, out ISequentialInStream inStream) { index -= _indexOffset; if (_files != null) { _fileStream = null; try { if (File.Exists(_files[index].FullName)) { _fileStream = new InStreamWrapper( new FileStream(_files[index].FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), true); } } catch (Exception e) { AddException(e); inStream = null; return -1; } inStream = _fileStream; if (!EventsForGetStream(index)) { return -1; } } else { if (_streamEntries == null) { inStream = _fileStream; } else { _fileStream = new InStreamWrapper(_streamEntries[index].Stream, true); inStream = _fileStream; if (!EventsForGetStream(index)) { return -1; } } } return 0; } public long EnumProperties(IntPtr enumerator) { //Not implemented HRESULT return 0x80004001L; } public void SetOperationResult(OperationResult operationResult) { if (operationResult != OperationResult.Ok && ReportErrors) { switch (operationResult) { case OperationResult.CrcError: AddException(new ExtractionFailedException("File is corrupted. Crc check has failed.")); break; case OperationResult.DataError: AddException(new ExtractionFailedException("File is corrupted. Data error has occurred.")); break; case OperationResult.UnsupportedMethod: AddException(new ExtractionFailedException("Unsupported method error has occurred.")); break; case OperationResult.Unavailable: AddException(new ExtractionFailedException("File is unavailable.")); break; case OperationResult.UnexpectedEnd: AddException(new ExtractionFailedException("Unexpected end of file.")); break; case OperationResult.DataAfterEnd: AddException(new ExtractionFailedException("Data after end of archive.")); break; case OperationResult.IsNotArc: AddException(new ExtractionFailedException("File is not archive.")); break; case OperationResult.HeadersError: AddException(new ExtractionFailedException("Archive headers error.")); break; case OperationResult.WrongPassword: AddException(new ExtractionFailedException("Wrong password.")); break; default: AddException(new ExtractionFailedException($"Unexpected operation result: {operationResult}")); break; } } if (_fileStream != null) { _fileStream.BytesRead -= IntEventArgsHandler; //Specific Zip implementation - can not Dispose files for Zip. if (_compressor.ArchiveFormat != OutArchiveFormat.Zip) { try { _fileStream.Dispose(); } catch (ObjectDisposedException) { } } else { _wrappersToDispose.Add(_fileStream); } _fileStream = null; } OnFileCompressionFinished(EventArgs.Empty); } #endregion #region ICryptoGetTextPassword2 Members public int CryptoGetTextPassword2(ref int passwordIsDefined, out string password) { passwordIsDefined = string.IsNullOrEmpty(Password) ? 0 : 1; password = Password; return 0; } #endregion #region IDisposable Members public void Dispose() { GC.RemoveMemoryPressure(_memoryPressure); if (_fileStream != null) { try { _fileStream.Dispose(); } catch (ObjectDisposedException) { } } if (_wrappersToDispose == null) { return; } foreach (var wrapper in _wrappersToDispose) { try { wrapper.Dispose(); } catch (ObjectDisposedException) { } } } #endregion private void IntEventArgsHandler(object sender, IntEventArgs e) { var lockObject = ((object)_files ?? _streamEntries) ?? _fileStream; lock (lockObject) { var pOld = (byte)(_bytesWrittenOld * 100 / _bytesCount); _bytesWritten += e.Value; byte pNow; if (_bytesCount < _bytesWritten) //Holy shit, this check for ZIP is golden { pNow = 100; } else { pNow = (byte)((_bytesWritten * 100) / _bytesCount); } if (pNow > pOld) { _bytesWrittenOld = _bytesWritten; OnCompressing(new ProgressEventArgs(pNow, (byte)(pNow - pOld))); } } } } #endif