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