namespace SevenZip
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
#if UNMANAGED
///
/// Archive extraction callback to handle the process of unpacking files
///
internal sealed class ArchiveExtractCallback : CallbackBase, IArchiveExtractCallback, ICryptoGetTextPassword, IDisposable
{
private List _actualIndexes;
private IInArchive _archive;
///
/// For Compressing event.
///
private long _bytesCount;
private long _bytesWritten;
private long _bytesWrittenOld;
private string _directory;
///
/// Rate of the done work from [0, 1].
///
private float _doneRate;
private SevenZipExtractor _extractor;
private FakeOutStreamWrapper _fakeStream;
private uint? _fileIndex;
private int _filesCount;
private OutStreamWrapper _fileStream;
private bool _directoryStructure;
private int _currentIndex;
private const int MemoryPressure = 64 * 1024 * 1024; //64mb seems to be the maximum value
#region Constructors
///
/// Initializes a new instance of the ArchiveExtractCallback class
///
/// IInArchive interface for the archive
/// Directory where files are to be unpacked to
/// The archive files count'
/// The owner of the callback
/// The list of actual indexes (solid archives support)
/// The value indicating whether to preserve directory structure of extracted files.
public ArchiveExtractCallback(IInArchive archive, string directory, int filesCount, bool directoryStructure,
List actualIndexes, SevenZipExtractor extractor)
{
Init(archive, directory, filesCount, directoryStructure, actualIndexes, extractor);
}
///
/// Initializes a new instance of the ArchiveExtractCallback class
///
/// IInArchive interface for the archive
/// Directory where files are to be unpacked to
/// The archive files count
/// Password for the archive
/// The owner of the callback
/// The list of actual indexes (solid archives support)
/// The value indicating whether to preserve directory structure of extracted files.
public ArchiveExtractCallback(IInArchive archive, string directory, int filesCount, bool directoryStructure,
List actualIndexes, string password, SevenZipExtractor extractor)
: base(password)
{
Init(archive, directory, filesCount, directoryStructure, actualIndexes, extractor);
}
///
/// Initializes a new instance of the ArchiveExtractCallback class
///
/// IInArchive interface for the archive
/// The stream where files are to be unpacked to
/// The archive files count
/// The file index for the stream
/// The owner of the callback
public ArchiveExtractCallback(IInArchive archive, Stream stream, int filesCount, uint fileIndex, SevenZipExtractor extractor)
{
Init(archive, stream, filesCount, fileIndex, extractor);
}
///
/// Initializes a new instance of the ArchiveExtractCallback class
///
/// IInArchive interface for the archive
/// The stream where files are to be unpacked to
/// The archive files count
/// The file index for the stream
/// Password for the archive
/// The owner of the callback
public ArchiveExtractCallback(IInArchive archive, Stream stream, int filesCount, uint fileIndex, string password, SevenZipExtractor extractor)
: base(password)
{
Init(archive, stream, filesCount, fileIndex, extractor);
}
private void Init(IInArchive archive, string directory, int filesCount, bool directoryStructure, List actualIndexes, SevenZipExtractor extractor)
{
CommonInit(archive, filesCount, extractor);
_directory = directory;
_actualIndexes = actualIndexes;
_directoryStructure = directoryStructure;
if (!directory.EndsWith("" + Path.DirectorySeparatorChar, StringComparison.CurrentCulture))
{
_directory += Path.DirectorySeparatorChar;
}
}
private void Init(IInArchive archive, Stream stream, int filesCount, uint fileIndex, SevenZipExtractor extractor)
{
CommonInit(archive, filesCount, extractor);
_fileStream = new OutStreamWrapper(stream, false);
_fileStream.BytesWritten += IntEventArgsHandler;
_fileIndex = fileIndex;
}
private void CommonInit(IInArchive archive, int filesCount, SevenZipExtractor extractor)
{
_archive = archive;
_filesCount = filesCount;
_fakeStream = new FakeOutStreamWrapper();
_fakeStream.BytesWritten += IntEventArgsHandler;
_extractor = extractor;
GC.AddMemoryPressure(MemoryPressure);
}
#endregion
///
/// Occurs when a new file is going to be unpacked
///
/// Occurs when 7-zip engine requests for an output stream for a new file to unpack in
public event EventHandler FileExtractionStarted;
///
/// Occurs when a file has been successfully unpacked
///
public event EventHandler FileExtractionFinished;
///
/// Occurs when the archive is opened and 7-zip sends the size of unpacked data
///
public event EventHandler Open;
///
/// Occurs when the extraction is performed
///
public event EventHandler Extracting;
///
/// Occurs during the extraction when a file already exists
///
public event EventHandler FileExists;
private void IntEventArgsHandler(object sender, IntEventArgs e)
{
// If _bytesCount is not set, we can't update the progress.
if (_bytesCount == 0)
{
return;
}
var pold = (int)(_bytesWrittenOld * 100 / _bytesCount);
_bytesWritten += e.Value;
var pnow = (int)(_bytesWritten * 100 / _bytesCount);
if (pnow > pold)
{
if (pnow > 100)
{
pold = pnow = 0;
}
_bytesWrittenOld = _bytesWritten;
Extracting?.Invoke(this, new ProgressEventArgs((byte)pnow, (byte)(pnow - pold)));
}
}
#region IArchiveExtractCallback Members
///
/// Gives the size of the unpacked archive files
///
/// Size of the unpacked archive files (in bytes)
public void SetTotal(ulong total)
{
_bytesCount = (long)total;
Open?.Invoke(this, new OpenEventArgs(total));
}
public void SetCompleted(ref ulong completeValue) { }
///
/// Sets output stream for writing unpacked data
///
/// Current file index
/// Output stream pointer
/// Extraction mode
/// 0 if OK
public int GetStream(uint index, out ISequentialOutStream outStream, AskMode askExtractMode)
{
outStream = null;
if (Canceled)
{
return -1;
}
_currentIndex = (int)index;
if (askExtractMode == AskMode.Extract)
{
var fileName = _directory;
if (!_fileIndex.HasValue)
{
// Extraction to a file
if (_actualIndexes == null || _actualIndexes.Contains(index))
{
var data = new PropVariant();
_archive.GetProperty(index, ItemPropId.Path, ref data);
var entryName = NativeMethods.SafeCast(data, "");
#region Get entryName
if (string.IsNullOrEmpty(entryName))
{
if (_filesCount == 1)
{
var archName = Path.GetFileName(_extractor.FileName);
archName = archName.Substring(0,
archName.LastIndexOf('.'));
if (!archName.EndsWith(".tar",
StringComparison.OrdinalIgnoreCase))
{
archName += ".tar";
}
entryName = archName;
}
else
{
entryName = "[no name] " + index.ToString(CultureInfo.InvariantCulture);
}
}
#endregion
try
{
fileName = Path.Combine(RemoveIllegalCharacters(_directory, true), RemoveIllegalCharacters(_directoryStructure ? entryName : Path.GetFileName(entryName)));
if (string.IsNullOrEmpty(fileName))
{
throw new SevenZipArchiveException("Some archive name is null or empty.");
}
}
catch (Exception e)
{
AddException(e);
outStream = _fakeStream;
return 0;
}
_archive.GetProperty(index, ItemPropId.IsDirectory, ref data);
if (!NativeMethods.SafeCast(data, false))
{
_archive.GetProperty(index, ItemPropId.LastWriteTime, ref data);
var time = NativeMethods.SafeCast(data, DateTime.MinValue);
if (File.Exists(fileName))
{
var fnea = new FileOverwriteEventArgs(fileName);
FileExists?.Invoke(this, fnea);
if (fnea.Cancel)
{
Canceled = true;
return -1;
}
if (string.IsNullOrEmpty(fnea.FileName))
{
outStream = _fakeStream;
}
else
{
fileName = fnea.FileName;
}
}
_doneRate += 1.0f / _filesCount;
var iea = new FileInfoEventArgs(_extractor.ArchiveFileData[(int) index], PercentDoneEventArgs.ProducePercentDone(_doneRate));
FileExtractionStarted?.Invoke(this, iea);
if (iea.Cancel)
{
Canceled = true;
return -1;
}
if (iea.Skip)
{
outStream = _fakeStream;
return 0;
}
CreateDirectory(fileName);
try
{
_fileStream = new OutStreamWrapper(File.Create(fileName), fileName, time, true);
}
catch (Exception e)
{
AddException(e is FileNotFoundException
? new IOException($"The file \"{fileName}\" was not extracted due to the File.Create fail.")
: e);
outStream = _fakeStream;
return 0;
}
_fileStream.BytesWritten += IntEventArgsHandler;
outStream = _fileStream;
}
else
{
_doneRate += 1.0f / _filesCount;
var iea = new FileInfoEventArgs(_extractor.ArchiveFileData[(int)index], PercentDoneEventArgs.ProducePercentDone(_doneRate));
FileExtractionStarted?.Invoke(this, iea);
if (iea.Cancel)
{
Canceled = true;
return -1;
}
if (iea.Skip)
{
outStream = _fakeStream;
return 0;
}
if (!Directory.Exists(fileName))
{
try
{
Directory.CreateDirectory(fileName);
}
catch (Exception e)
{
AddException(e);
}
outStream = _fakeStream;
}
}
}
else
{
outStream = _fakeStream;
}
}
else
{
// Extraction to a stream.
if (index == _fileIndex)
{
outStream = _fileStream;
_fileIndex = null;
}
else
{
outStream = _fakeStream;
}
}
}
return 0;
}
///
public void PrepareOperation(AskMode askExtractMode) { }
///
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 occured."));
break;
case OperationResult.UnsupportedMethod:
AddException(new ExtractionFailedException("Unsupported method error has occured."));
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;
}
}
else
{
if (_fileStream != null && !_fileIndex.HasValue)
{
try
{
_fileStream.BytesWritten -= IntEventArgsHandler;
_fileStream.Dispose();
}
catch (ObjectDisposedException) { }
_fileStream = null;
}
var iea = new FileInfoEventArgs(_extractor.ArchiveFileData[_currentIndex], PercentDoneEventArgs.ProducePercentDone(_doneRate));
FileExtractionFinished?.Invoke(this, iea);
if (iea.Cancel)
{
Canceled = true;
}
}
}
#endregion
///
public int CryptoGetTextPassword(out string password)
{
password = Password;
return 0;
}
///
public void Dispose()
{
GC.RemoveMemoryPressure(MemoryPressure);
if (_fileStream != null)
{
try
{
_fileStream.Dispose();
}
catch (ObjectDisposedException) { }
_fileStream = null;
}
if (_fakeStream != null)
{
try
{
_fakeStream.Dispose();
}
catch (ObjectDisposedException) { }
_fakeStream = null;
}
}
///
/// Ensures that the directory to the file name is valid and creates intermediate directories if necessary
///
/// File name
private static void CreateDirectory(string fileName)
{
var destinationDirectory = Path.GetDirectoryName(fileName);
if (!string.IsNullOrEmpty(destinationDirectory))
{
Directory.CreateDirectory(destinationDirectory);
}
}
///
/// removes the invalid character in file path.
///
///
///
///
private static string RemoveIllegalCharacters(string str, bool isDirectory = false)
{
var splitFileName = new List(str.Split(Path.DirectorySeparatorChar));
foreach (var chr in Path.GetInvalidFileNameChars())
{
for (var i = 0; i < splitFileName.Count; i++)
{
if (isDirectory && chr == ':' && i == 0)
{
continue;
}
if (string.IsNullOrEmpty(splitFileName[i]))
{
continue;
}
while (splitFileName[i].IndexOf(chr) > -1)
{
splitFileName[i] = splitFileName[i].Replace(chr, '_');
}
}
}
if (str.StartsWith(new string(Path.DirectorySeparatorChar, 2), StringComparison.CurrentCultureIgnoreCase))
{
splitFileName.RemoveAt(0);
splitFileName.RemoveAt(0);
splitFileName[0] = new string(Path.DirectorySeparatorChar, 2) + splitFileName[0];
}
return string.Join(new string(Path.DirectorySeparatorChar, 1), splitFileName.ToArray());
}
}
#endif
}