using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Collections;
/* Author: Michael A. McCloskey
* Company: Schematrix
* Version: 20040714
*
* Personal Comments:
* I created this unrar wrapper class for personal use
* after running into a number of issues trying to use
* another COM unrar product via COM interop. I hope it
* proves as useful to you as it has to me and saves you
* some time in building your own products.
*/
namespace UnRarDotNet
{
#region Event Delegate Definitions
///
/// Represents the method that will handle data available events
///
public delegate void DataAvailableHandler(object sender, DataAvailableEventArgs e);
///
/// Represents the method that will handle extraction progress events
///
public delegate void ExtractionProgressHandler(object sender, ExtractionProgressEventArgs e);
///
/// Represents the method that will handle missing archive volume events
///
public delegate void MissingVolumeHandler(object sender, MissingVolumeEventArgs e);
///
/// Represents the method that will handle new volume events
///
public delegate void NewVolumeHandler(object sender, NewVolumeEventArgs e);
///
/// Represents the method that will handle new file notifications
///
public delegate void NewFileHandler(object sender, NewFileEventArgs e);
///
/// Represents the method that will handle password required events
///
public delegate void PasswordRequiredHandler(object sender, PasswordRequiredEventArgs e);
#endregion
///
/// Wrapper class for unrar DLL supplied by RARSoft.
/// Calls unrar DLL via platform invocation services (pinvoke).
/// DLL is available at http://www.rarlab.com/rar/UnRARDLL.exe
///
public class Unrar : IDisposable
{
#region Unrar DLL enumerations
///
/// Mode in which archive is to be opened for processing.
///
public enum OpenMode
{
///
/// Open archive for listing contents only
///
List=0,
///
/// Open archive for testing or extracting contents
///
Extract=1
}
private enum RarError : uint
{
EndOfArchive=10,
InsufficientMemory=11,
BadData=12,
BadArchive=13,
UnknownFormat=14,
OpenError=15,
CreateError=16,
CloseError=17,
ReadError=18,
WriteError=19,
BufferTooSmall=20,
UnknownError=21
}
private enum Operation : uint
{
Skip=0,
Test=1,
Extract=2
}
private enum VolumeMessage : uint
{
Ask=0,
Notify=1
}
[Flags]
private enum ArchiveFlags : uint
{
Volume=0x1, // Volume attribute (archive volume)
CommentPresent=0x2, // Archive comment present
Lock=0x4, // Archive lock attribute
SolidArchive=0x8, // Solid attribute (solid archive)
NewNamingScheme=0x10, // New volume naming scheme ('volname.partN.rar')
AuthenticityPresent=0x20, // Authenticity information present
RecoveryRecordPresent=0x40, // Recovery record present
EncryptedHeaders=0x80, // Block headers are encrypted
FirstVolume=0x100 // 0x0100 - First volume (set only by RAR 3.0 and later)
}
private enum CallbackMessages : uint
{
VolumeChange=0,
ProcessData=1,
NeedPassword=2
}
#endregion
#region Unrar DLL structure definitions
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
private struct RARHeaderData
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=260)]
public string ArcName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=260)]
public string FileName;
public uint Flags;
public uint PackSize;
public uint UnpSize;
public uint HostOS;
public uint FileCRC;
public uint FileTime;
public uint UnpVer;
public uint Method;
public uint FileAttr;
[MarshalAs(UnmanagedType.LPStr)]
public string CmtBuf;
public uint CmtBufSize;
public uint CmtSize;
public uint CmtState;
public void Initialize()
{
this.CmtBuf=new string((char)0, 65536);
this.CmtBufSize=65536;
}
}
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public struct RARHeaderDataEx
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=512)]
public string ArcName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=1024)]
public string ArcNameW;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=512)]
public string FileName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=1024)]
public string FileNameW;
public uint Flags;
public uint PackSize;
public uint PackSizeHigh;
public uint UnpSize;
public uint UnpSizeHigh;
public uint HostOS;
public uint FileCRC;
public uint FileTime;
public uint UnpVer;
public uint Method;
public uint FileAttr;
[MarshalAs(UnmanagedType.LPStr)]
public string CmtBuf;
public uint CmtBufSize;
public uint CmtSize;
public uint CmtState;
[MarshalAs(UnmanagedType.ByValArray, SizeConst=1024)]
public uint[] Reserved;
public void Initialize()
{
this.CmtBuf=new string((char)0, 65536);
this.CmtBufSize=65536;
}
}
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
public struct RAROpenArchiveData
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=260)]
public string ArcName;
public uint OpenMode;
public uint OpenResult;
[MarshalAs(UnmanagedType.LPStr)]
public string CmtBuf;
public uint CmtBufSize;
public uint CmtSize;
public uint CmtState;
public void Initialize()
{
this.CmtBuf=new string((char)0,65536);
this.CmtBufSize=65536;
}
}
[StructLayout(LayoutKind.Sequential)]
public struct RAROpenArchiveDataEx
{
[MarshalAs(UnmanagedType.LPStr)]
public string ArcName;
[MarshalAs(UnmanagedType.LPWStr)]
public string ArcNameW;
public uint OpenMode;
public uint OpenResult;
[MarshalAs(UnmanagedType.LPStr)]
public string CmtBuf;
public uint CmtBufSize;
public uint CmtSize;
public uint CmtState;
public uint Flags;
[MarshalAs(UnmanagedType.ByValArray, SizeConst=32)]
public uint[] Reserved;
public void Initialize()
{
this.CmtBuf=new string((char)0,65536);
this.CmtBufSize=65536;
this.Reserved=new uint[32];
}
}
#endregion
#region Unrar function declarations
[DllImport("unrar.dll")]
private static extern IntPtr RAROpenArchive(ref RAROpenArchiveData archiveData);
[DllImport("UNRAR.DLL")]
private static extern IntPtr RAROpenArchiveEx(ref RAROpenArchiveDataEx archiveData);
[DllImport("unrar.dll")]
private static extern int RARCloseArchive(IntPtr hArcData);
[DllImport("unrar.dll")]
private static extern int RARReadHeader(IntPtr hArcData, ref RARHeaderData headerData);
[DllImport("unrar.dll")]
private static extern int RARReadHeaderEx(IntPtr hArcData, ref RARHeaderDataEx headerData);
[DllImport("unrar.dll")]
private static extern int RARProcessFile(IntPtr hArcData, int operation,
[MarshalAs(UnmanagedType.LPStr)] string destPath,
[MarshalAs(UnmanagedType.LPStr)] string destName );
[DllImport("unrar.dll")]
private static extern void RARSetCallback(IntPtr hArcData, UNRARCallback callback, int userData);
[DllImport("unrar.dll")]
private static extern void RARSetPassword(IntPtr hArcData,
[MarshalAs(UnmanagedType.LPStr)] string password);
// Unrar callback delegate signature
private delegate int UNRARCallback(uint msg, int UserData, IntPtr p1, int p2);
#endregion
#region Public event declarations
///
/// Event that is raised when a new chunk of data has been extracted
///
public event DataAvailableHandler DataAvailable;
///
/// Event that is raised to indicate extraction progress
///
public event ExtractionProgressHandler ExtractionProgress;
///
/// Event that is raised when a required archive volume is missing
///
public event MissingVolumeHandler MissingVolume;
///
/// Event that is raised when a new file is encountered during processing
///
public event NewFileHandler NewFile;
///
/// Event that is raised when a new archive volume is opened for processing
///
public event NewVolumeHandler NewVolume;
///
/// Event that is raised when a password is required before continuing
///
public event PasswordRequiredHandler PasswordRequired;
#endregion
#region Private fields
private string archivePathName=string.Empty;
private IntPtr archiveHandle=new IntPtr(0);
private bool retrieveComment=true;
private string password=string.Empty;
private string comment=string.Empty;
private ArchiveFlags archiveFlags=0;
private RARHeaderDataEx header=new RARHeaderDataEx();
private string destinationPath=string.Empty;
private RARFileInfo currentFile=null;
private UNRARCallback callback=null;
#endregion
#region Object lifetime procedures
public Unrar()
{
this.callback=new UNRARCallback(RARCallback);
}
public Unrar(string archivePathName) : this()
{
this.archivePathName=archivePathName;
}
~Unrar()
{
if(this.archiveHandle!=IntPtr.Zero)
{
Unrar.RARCloseArchive(this.archiveHandle);
this.archiveHandle=IntPtr.Zero;
}
}
public void Dispose()
{
if(this.archiveHandle!=IntPtr.Zero)
{
Unrar.RARCloseArchive(this.archiveHandle);
this.archiveHandle=IntPtr.Zero;
}
}
#endregion
#region Public Properties
///
/// Path and name of RAR archive to open
///
public string ArchivePathName
{
get
{
return this.archivePathName;
}
set
{
this.archivePathName=value;
}
}
///
/// Archive comment
///
public string Comment
{
get
{
return(this.comment);
}
}
///
/// Current file being processed
///
public RARFileInfo CurrentFile
{
get
{
return(this.currentFile);
}
}
///
/// Default destination path for extraction
///
public string DestinationPath
{
get
{
return this.destinationPath;
}
set
{
this.destinationPath=value;
}
}
///
/// Password for opening encrypted archive
///
public string Password
{
get
{
return(this.password);
}
set
{
this.password=value;
if(this.archiveHandle!=IntPtr.Zero)
RARSetPassword(this.archiveHandle, value);
}
}
#endregion
#region Public Methods
///
/// Close the currently open archive
///
///
public void Close()
{
// Exit without exception if no archive is open
if(this.archiveHandle==IntPtr.Zero)
return;
// Close archive
int result=Unrar.RARCloseArchive(this.archiveHandle);
// Check result
if(result!=0)
{
ProcessFileError(result);
}
else
{
this.archiveHandle=IntPtr.Zero;
}
}
///
/// Opens archive specified by the ArchivePathName property for testing or extraction
///
public void Open()
{
if(this.ArchivePathName.Length==0)
throw new IOException("Archive name has not been set.");
this.Open(this.ArchivePathName, OpenMode.Extract);
}
///
/// Opens archive specified by the ArchivePathName property with a specified mode
///
/// Mode in which archive should be opened
public void Open(OpenMode openMode)
{
if(this.ArchivePathName.Length==0)
throw new IOException("Archive name has not been set.");
this.Open(this.ArchivePathName, openMode);
}
///
/// Opens specified archive using the specified mode.
///
/// Path of archive to open
/// Mode in which to open archive
public void Open(string archivePathName, OpenMode openMode)
{
IntPtr handle=IntPtr.Zero;
// Close any previously open archives
if(this.archiveHandle!=IntPtr.Zero)
this.Close();
// Prepare extended open archive struct
this.ArchivePathName=archivePathName;
RAROpenArchiveDataEx openStruct=new RAROpenArchiveDataEx();
openStruct.Initialize();
openStruct.ArcName=this.archivePathName+"\0";
openStruct.ArcNameW=this.archivePathName+"\0";
openStruct.OpenMode=(uint)openMode;
if(this.retrieveComment)
{
openStruct.CmtBuf=new string((char)0,65536);
openStruct.CmtBufSize=65536;
}
else
{
openStruct.CmtBuf=null;
openStruct.CmtBufSize=0;
}
// Open archive
handle=Unrar.RAROpenArchiveEx(ref openStruct);
// Check for success
if(openStruct.OpenResult!=0)
{
switch((RarError)openStruct.OpenResult)
{
case RarError.InsufficientMemory:
throw new OutOfMemoryException("Insufficient memory to perform operation.");
case RarError.BadData:
throw new IOException("Archive header broken");
case RarError.BadArchive:
throw new IOException("File is not a valid archive.");
case RarError.OpenError:
throw new IOException("File could not be opened.");
}
}
// Save handle and flags
this.archiveHandle=handle;
this.archiveFlags=(ArchiveFlags)openStruct.Flags;
// Set callback
Unrar.RARSetCallback(this.archiveHandle, this.callback, this.GetHashCode());
// If comment retrieved, save it
if(openStruct.CmtState==1)
this.comment=openStruct.CmtBuf.ToString();
// If password supplied, set it
if(this.password.Length!=0)
Unrar.RARSetPassword(this.archiveHandle, this.password);
// Fire NewVolume event for first volume
this.OnNewVolume(this.archivePathName);
}
///
/// Reads the next archive header and populates CurrentFile property data
///
///
public bool ReadHeader()
{
// Throw exception if archive not open
if(this.archiveHandle==IntPtr.Zero)
throw new IOException("Archive is not open.");
// Initialize header struct
this.header=new RARHeaderDataEx();
header.Initialize();
// Read next entry
currentFile=null;
int result=Unrar.RARReadHeaderEx(this.archiveHandle, ref this.header);
// Check for error or end of archive
if((RarError)result==RarError.EndOfArchive)
return false;
else if((RarError)result==RarError.BadData)
throw new IOException("Archive data is corrupt.");
// Determine if new file
if(((header.Flags & 0x01) != 0) && currentFile!=null)
currentFile.ContinuedFromPrevious=true;
else
{
// New file, prepare header
currentFile=new RARFileInfo();
currentFile.FileName=header.FileNameW.ToString();
if((header.Flags & 0x02) != 0)
currentFile.ContinuedOnNext=true;
if(header.PackSizeHigh != 0)
currentFile.PackedSize=(header.PackSizeHigh * 0x100000000) + header.PackSize;
else
currentFile.PackedSize=header.PackSize;
if(header.UnpSizeHigh != 0)
currentFile.UnpackedSize=(header.UnpSizeHigh * 0x100000000) + header.UnpSize;
else
currentFile.UnpackedSize=header.UnpSize;
currentFile.HostOS=(int)header.HostOS;
currentFile.FileCRC=header.FileCRC;
currentFile.FileTime=FromMSDOSTime(header.FileTime);
currentFile.VersionToUnpack=(int)header.UnpVer;
currentFile.Method=(int)header.Method;
currentFile.FileAttributes=(int)header.FileAttr;
currentFile.BytesExtracted=0;
if((header.Flags & 0xE0) == 0xE0)
currentFile.IsDirectory=true;
this.OnNewFile();
}
// Return success
return true;
}
///
/// Returns array of file names contained in archive
///
///
public string[] ListFiles()
{
ArrayList fileNames=new ArrayList();
while(this.ReadHeader())
{
if(!currentFile.IsDirectory)
fileNames.Add(currentFile.FileName);
this.Skip();
}
string[] files=new string[fileNames.Count];
fileNames.CopyTo(files);
return files;
}
///
/// Moves the current archive position to the next available header
///
///
public void Skip()
{
int result=Unrar.RARProcessFile(this.archiveHandle, (int)Operation.Skip, string.Empty, string.Empty);
// Check result
if(result!=0)
{
ProcessFileError(result);
}
}
///
/// Tests the ability to extract the current file without saving extracted data to disk
///
///
public void Test()
{
int result=Unrar.RARProcessFile(this.archiveHandle, (int)Operation.Test, string.Empty, string.Empty);
// Check result
if(result!=0)
{
ProcessFileError(result);
}
}
///
/// Extracts the current file to the default destination path
///
///
public void Extract()
{
this.Extract(this.destinationPath, string.Empty);
}
///
/// Extracts the current file to a specified destination path and filename
///
/// Path and name of extracted file
///
public void Extract(string destinationName)
{
this.Extract(string.Empty, destinationName);
}
///
/// Extracts the current file to a specified directory without renaming file
///
///
///
public void ExtractToDirectory(string destinationPath)
{
this.Extract(destinationPath, string.Empty);
}
#endregion
#region Private Methods
private void Extract(string destinationPath, string destinationName)
{
int result=Unrar.RARProcessFile(this.archiveHandle, (int)Operation.Extract, destinationPath, destinationName);
// Check result
if(result!=0)
{
ProcessFileError(result);
}
}
private DateTime FromMSDOSTime(uint dosTime)
{
int day=0;
int month=0;
int year=0;
int second=0;
int hour=0;
int minute=0;
ushort hiWord;
ushort loWord;
hiWord=(ushort)((dosTime & 0xFFFF0000) >> 16);
loWord=(ushort)(dosTime & 0xFFFF);
year=((hiWord & 0xFE00) >> 9)+1980;
month=(hiWord & 0x01E0) >> 5;
day=hiWord & 0x1F;
hour=(loWord & 0xF800) >> 11;
minute=(loWord & 0x07E0) >> 5;
second=(loWord & 0x1F) << 1;
return new DateTime(year, month, day, hour, minute, second);
}
private void ProcessFileError(int result)
{
switch((RarError)result)
{
case RarError.UnknownFormat:
throw new OutOfMemoryException("Unknown archive format.");
case RarError.BadData:
throw new IOException("File CRC Error");
case RarError.BadArchive:
throw new IOException("File is not a valid archive.");
case RarError.OpenError:
throw new IOException("File could not be opened.");
case RarError.CreateError:
throw new IOException("File could not be created.");
case RarError.CloseError:
throw new IOException("File close error.");
case RarError.ReadError:
throw new IOException("File read error.");
case RarError.WriteError:
throw new IOException("File write error.");
}
}
private int RARCallback(uint msg, int UserData, IntPtr p1, int p2)
{
string volume=string.Empty;
string newVolume=string.Empty;
int result=-1;
switch((CallbackMessages)msg)
{
case CallbackMessages.VolumeChange:
volume=Marshal.PtrToStringAnsi(p1);
if((VolumeMessage)p2==VolumeMessage.Notify)
result=OnNewVolume(volume);
else if((VolumeMessage)p2==VolumeMessage.Ask)
{
newVolume=OnMissingVolume(volume);
if(newVolume.Length==0)
result=-1;
else
{
if(newVolume!=volume)
{
for(int i=0; i0)
{
for(int i=0; (i