diff --git a/CUETools/CUETools.sln b/CUETools/CUETools.sln index a618e4f..0bc6484 100644 --- a/CUETools/CUETools.sln +++ b/CUETools/CUETools.sln @@ -47,6 +47,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AudioCodecsDotNet", "..\Aud EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ALACDotNet", "..\ALACDotNet\ALACDotNet.csproj", "{F2EC7193-D5E5-4252-9803-5CEB407E910F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnRarDotNet", "..\UnRarDotNet\UnRarDotNet.csproj", "{8427CAA5-80B8-4952-9A68-5F3DFCFBDF40}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -200,6 +202,16 @@ Global {F2EC7193-D5E5-4252-9803-5CEB407E910F}.Release|x64.Build.0 = Release|x64 {F2EC7193-D5E5-4252-9803-5CEB407E910F}.Release|x86.ActiveCfg = Release|x86 {F2EC7193-D5E5-4252-9803-5CEB407E910F}.Release|x86.Build.0 = Release|x86 + {8427CAA5-80B8-4952-9A68-5F3DFCFBDF40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8427CAA5-80B8-4952-9A68-5F3DFCFBDF40}.Debug|x64.ActiveCfg = Debug|x64 + {8427CAA5-80B8-4952-9A68-5F3DFCFBDF40}.Debug|x64.Build.0 = Debug|x64 + {8427CAA5-80B8-4952-9A68-5F3DFCFBDF40}.Debug|x86.ActiveCfg = Debug|x86 + {8427CAA5-80B8-4952-9A68-5F3DFCFBDF40}.Debug|x86.Build.0 = Debug|x86 + {8427CAA5-80B8-4952-9A68-5F3DFCFBDF40}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8427CAA5-80B8-4952-9A68-5F3DFCFBDF40}.Release|x64.ActiveCfg = Release|x64 + {8427CAA5-80B8-4952-9A68-5F3DFCFBDF40}.Release|x64.Build.0 = Release|x64 + {8427CAA5-80B8-4952-9A68-5F3DFCFBDF40}.Release|x86.ActiveCfg = Release|x86 + {8427CAA5-80B8-4952-9A68-5F3DFCFBDF40}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/CUEToolsLib/AudioReadWrite.cs b/CUEToolsLib/AudioReadWrite.cs index b8e0e4a..bf987b2 100644 --- a/CUEToolsLib/AudioReadWrite.cs +++ b/CUEToolsLib/AudioReadWrite.cs @@ -28,6 +28,18 @@ namespace CUEToolsLib { throw new Exception("Unsupported audio type."); } } + public static IAudioSource GetAudioSource(string path, Stream IO) + { + switch (Path.GetExtension(path).ToLower()) + { +#if !MONO + case ".flac": + return new FLACReader(path, IO); +#endif + default: + throw new Exception("Unsupported audio type in archive."); + } + } public static IAudioDest GetAudioDest(string path, int bitsPerSample, int channelCount, int sampleRate, long finalSampleCount) { IAudioDest dest; @@ -59,7 +71,14 @@ namespace CUEToolsLib { uint _bufferOffset, _bufferLength; public FLACReader(string path) { - _flacReader = new FLACDotNet.FLACReader(path); + _flacReader = new FLACDotNet.FLACReader(path, null); + _bufferOffset = 0; + _bufferLength = 0; + } + + public FLACReader(string path, Stream IO) + { + _flacReader = new FLACDotNet.FLACReader(path, IO); _bufferOffset = 0; _bufferLength = 0; } diff --git a/CUEToolsLib/CUEToolsLib.csproj b/CUEToolsLib/CUEToolsLib.csproj index ef4f6c9..57b1fb1 100644 --- a/CUEToolsLib/CUEToolsLib.csproj +++ b/CUEToolsLib/CUEToolsLib.csproj @@ -109,6 +109,10 @@ {32338A04-5B6B-4C63-8EE7-C6400F73B5D7} HDCDDotNet + + {8427CAA5-80B8-4952-9A68-5F3DFCFBDF40} + UnRarDotNet + {CC2E74B6-534A-43D8-9F16-AC03FE955000} WavPackDotNet diff --git a/CUEToolsLib/Main.cs b/CUEToolsLib/Main.cs index bb14ee5..358e1f6 100644 --- a/CUEToolsLib/Main.cs +++ b/CUEToolsLib/Main.cs @@ -29,6 +29,9 @@ using System.Net; using System.Threading; using AudioCodecsDotNet; using HDCDDotNet; +#if !MONO +using UnRarDotNet; +#endif namespace CUEToolsLib { @@ -415,6 +418,8 @@ namespace CUEToolsLib private HDCDDotNet.HDCDDotNet hdcdDecoder; CUEConfig _config; string _cddbDiscIdTag; + private bool _isArchive; + private string _archivePath; public CUESheet(string pathIn, CUEConfig config) { @@ -466,6 +471,7 @@ namespace CUEToolsLib seenDataTrack = false; accDisks = new List(); _hasEmbeddedCUESheet = false; + _isArchive = false; TextReader sr; @@ -480,8 +486,56 @@ namespace CUEToolsLib if (cueSheet == null) throw new Exception("Input directory doesn't contain supported audio files."); sr = new StringReader(cueSheet); + } +#if !MONO + else if (Path.GetExtension(pathIn).ToLower() == ".rar") + { + Unrar _unrar = new Unrar(); + string cueName = null, cueText = null; + _unrar.Open(pathIn, Unrar.OpenMode.List); + while (_unrar.ReadHeader()) + { + if (!_unrar.CurrentFile.IsDirectory && Path.GetExtension(_unrar.CurrentFile.FileName).ToLower() == ".cue") + { + cueName = _unrar.CurrentFile.FileName; + break; + } + else + _unrar.Skip(); + } + _unrar.Close(); + if (cueName != null) + try + { + RarStream rarStream = new RarStream(pathIn, cueName); + StreamReader cueReader = new StreamReader(rarStream, CUESheet.Encoding); + cueText = cueReader.ReadToEnd(); + cueReader.Close(); + rarStream.Close(); + } + catch { } + if (cueText == null) + throw new Exception("Input archive doesn't contain a cue sheet."); + sr = new StringReader(cueText); + _isArchive = true; + _archivePath = pathIn; + } +#endif + else if (Path.GetExtension(pathIn).ToLower() == ".cue") + { + if (_config.autoCorrectFilenames) + sr = new StringReader (CorrectAudioFilenames(pathIn, false)); + else + sr = new StreamReader (pathIn, CUESheet.Encoding); + + try + { + StreamReader logReader = new StreamReader(Path.ChangeExtension(pathIn, ".log"), CUESheet.Encoding); + _eacLog = logReader.ReadToEnd(); + logReader.Close(); + } + catch { } } else - if (Path.GetExtension(pathIn).ToLower() != ".cue") { IAudioSource audioSource; NameValueCollection tags; @@ -500,20 +554,6 @@ namespace CUEToolsLib sr = new StringReader (cuesheetTag); pathAudio = pathIn; _hasEmbeddedCUESheet = true; - } else - { - if (_config.autoCorrectFilenames) - sr = new StringReader (CorrectAudioFilenames(pathIn, false)); - else - sr = new StreamReader (pathIn, CUESheet.Encoding); - - try - { - StreamReader logReader = new StreamReader(Path.ChangeExtension(pathIn, ".log"), CUESheet.Encoding); - _eacLog = logReader.ReadToEnd(); - logReader.Close(); - } - catch { } } using (sr) { @@ -533,10 +573,17 @@ namespace CUEToolsLib else { if (!_hasEmbeddedCUESheet) { - pathAudio = LocateFile(cueDir, line.Params[1]); - if (pathAudio == null) +#if !MONO + if (_isArchive) + pathAudio = line.Params[1]; + else { - throw new Exception("Unable to locate file \"" + line.Params[1] + "\"."); +#endif + pathAudio = LocateFile(cueDir, line.Params[1]); + if (pathAudio == null) + { + throw new Exception("Unable to locate file \"" + line.Params[1] + "\"."); + } } } else { @@ -645,6 +692,18 @@ namespace CUEToolsLib { _accurateRipId = line.Params[2]; } + //else if ((command == "REM") && + // (line.Params.Count == 3) && + // (line.Params[1].ToUpper() == "SHORTEN")) + //{ + // fileTimeLengthFrames -= General.TimeFromString(line.Params[2]); + //} + //else if ((command == "REM") && + // (line.Params.Count == 3) && + // (line.Params[1].ToUpper() == "LENGTHEN")) + //{ + // fileTimeLengthFrames += General.TimeFromString(line.Params[2]); + //} else { if (trackInfo != null) @@ -951,7 +1010,14 @@ namespace CUEToolsLib { IAudioSource audioSource; - audioSource = AudioReadWrite.GetAudioSource(path); +#if !MONO + if (_isArchive) + { + RarStream IO = new RarStream(_archivePath, path); + audioSource = AudioReadWrite.GetAudioSource(path, IO); + } else +#endif + audioSource = AudioReadWrite.GetAudioSource(path); if ((audioSource.BitsPerSample != 16) || (audioSource.ChannelCount != 2) || @@ -1686,7 +1752,7 @@ namespace CUEToolsLib if (_accurateRip) { statusDel((string)"Generating AccurateRip report...", 0, 0, null, null); - if (!_accurateOffset && _config.writeArTagsOnVerify && _writeOffset == 0) + if (!_accurateOffset && _config.writeArTagsOnVerify && _writeOffset == 0 && !_isArchive) { uint tracksMatch; int bestOffset; @@ -2141,7 +2207,15 @@ namespace CUEToolsLib return sw.ToString(); } - public static string CorrectAudioFilenames(string path, bool always) { + public static string CorrectAudioFilenames(string path, bool always) + { + StreamReader sr = new StreamReader(path, CUESheet.Encoding); + string cue = sr.ReadToEnd(); + sr.Close(); + return CorrectAudioFilenames(Path.GetDirectoryName(path), cue, always); + } + + public static string CorrectAudioFilenames(string dir, string cue, bool always) { string[] audioExts = new string[] { "*.wav", "*.flac", "*.wv", "*.ape", "*.m4a" }; List lines = new List(); List filePos = new List(); @@ -2150,12 +2224,9 @@ namespace CUEToolsLib string[] audioFiles = null; string lineStr; CUELine line; - string dir; int i; - dir = Path.GetDirectoryName(path); - - using (StreamReader sr = new StreamReader(path, CUESheet.Encoding)) { + using (StringReader sr = new StringReader(cue)) { while ((lineStr = sr.ReadLine()) != null) { lines.Add(lineStr); line = new CUELine(lineStr); @@ -2335,10 +2406,19 @@ namespace CUEToolsLib audioSource = new SilenceGenerator(sourceInfo.Offset + sourceInfo.Length); } else { - audioSource = AudioReadWrite.GetAudioSource(sourceInfo.Path); +#if !MONO + if (_isArchive) + { + RarStream IO = new RarStream(_archivePath, sourceInfo.Path); + audioSource = AudioReadWrite.GetAudioSource(sourceInfo.Path, IO); + } + else +#endif + audioSource = AudioReadWrite.GetAudioSource(sourceInfo.Path); } - audioSource.Position = sourceInfo.Offset; + if (sourceInfo.Offset != 0) + audioSource.Position = sourceInfo.Offset; return audioSource; } diff --git a/FLACDotNet/flacdotnet.cpp b/FLACDotNet/flacdotnet.cpp index 7a8d8dd..30a6f42 100644 --- a/FLACDotNet/flacdotnet.cpp +++ b/FLACDotNet/flacdotnet.cpp @@ -69,7 +69,7 @@ namespace FLACDotNet { public ref class FLACReader { public: - FLACReader(String^ path) { + FLACReader(String^ path, Stream^ IO) { _tags = gcnew NameValueCollection(); _writeDel = gcnew DecoderWriteDelegate(this, &FLACReader::WriteCallback); @@ -88,7 +88,10 @@ namespace FLACDotNet { _sampleBuffer = nullptr; _path = path; - _IO = gcnew FileStream (path, FileMode::Open, FileAccess::Read, FileShare::Read); + if (IO) + _IO = IO; + else + _IO = gcnew FileStream (path, FileMode::Open, FileAccess::Read, FileShare::Read); _decoder = FLAC__stream_decoder_new(); @@ -252,7 +255,10 @@ namespace FLACDotNet { _decoderActive = false; } if (_IO != nullptr) + { _IO->Close (); + _IO = nullptr; + } } Int32 Read([Out] array^% sampleBuffer) @@ -288,7 +294,7 @@ namespace FLACDotNet { NameValueCollection^ _tags; String^ _path; bool _decoderActive; - FileStream^ _IO; + Stream^ _IO; FLAC__StreamDecoderWriteStatus WriteCallback(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 * const buffer[], void *client_data) diff --git a/UnRarDotNet/Properties/AssemblyInfo.cs b/UnRarDotNet/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d6fdbc8 --- /dev/null +++ b/UnRarDotNet/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("UnRarDotNet")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("UnRarDotNet")] +[assembly: AssemblyCopyright("Copyright © Gregory S. Chudov, Michael A. McCloskey")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("87d4581b-faf5-4b85-999c-247f0a3eb64a")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/UnRarDotNet/RarStream.cs b/UnRarDotNet/RarStream.cs new file mode 100644 index 0000000..a1ea2d8 --- /dev/null +++ b/UnRarDotNet/RarStream.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Threading; + + +/* Author: Gregory S. Chudov + * + */ + +namespace UnRarDotNet +{ + public class RarStream : Stream + { + public RarStream(string archive, string fileName) + { + _stop = false; + _unrar = new Unrar(); + _buffer = null; + _offset = 0; + _length = 0; + _unrar.PasswordRequired += new PasswordRequiredHandler(unrar_PasswordRequired); + _unrar.DataAvailable += new DataAvailableHandler(unrar_DataAvailable); + _unrar.Open(archive, Unrar.OpenMode.Extract); + _fileName = fileName; + _workThread = new Thread(Decompress); + _workThread.Priority = ThreadPriority.BelowNormal; + _workThread.IsBackground = true; + _workThread.Start(null); + } + public override bool CanRead + { + get { return true; } + } + public override bool CanSeek + { + get { return false; } + } + public override bool CanWrite + { + get { return false; } + } + public override long Length + { + get { throw new NotSupportedException(); } + } + public override long Position + { + get { throw new NotSupportedException(); } + set { Seek(value, SeekOrigin.Begin); } + } + public override void Close() + { + lock (this) + { + _stop = true; + Monitor.Pulse(this); + } + _workThread.Join(); + _workThread = null; + _unrar.Close(); + base.Close(); + } + public override void Flush() + { + throw new NotSupportedException(); + } + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + public override int Read(byte[] array, int offset, int count) + { + int total = 0; + while (count > 0) + { + lock (this) + { + while (_buffer == null && !_stop) + Monitor.Wait(this); + if (_buffer == null) + return total; + if (_length > count) + { + Array.Copy(_buffer, _offset, array, offset, count); + total += count; + _offset += count; + _length -= count; + return total; + } + Array.Copy(_buffer, _offset, array, offset, _length); + total += _length; + offset += _length; + count -= _length; + _buffer = null; + Monitor.Pulse(this); + } + } + return total; + } + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + public override void Write(byte[] array, int offset, int count) + { + throw new NotSupportedException(); + } + + private Unrar _unrar; + private string _fileName; + private Thread _workThread; + private bool _stop; + private byte[] _buffer; + int _offset, _length; + + private void unrar_PasswordRequired(object sender, PasswordRequiredEventArgs e) + { + e.Password = "PARS"; + e.ContinueOperation = true; + } + + private void unrar_DataAvailable(object sender, DataAvailableEventArgs e) + { + lock (this) + { + while (_buffer != null && !_stop) + Monitor.Wait(this); + if (_stop) + { + e.ContinueOperation = false; + Monitor.Pulse(this); + return; + } + _buffer = e.Data; + _length = _buffer.Length; + _offset = 0; + e.ContinueOperation = true; + Monitor.Pulse(this); + } + } + + private void Decompress(object o) + { + //try + { + while (_unrar.ReadHeader()) + { + if (_unrar.CurrentFile.FileName == _fileName) + { + // unrar.CurrentFile.UnpackedSize; + _unrar.Test(); + lock (this) + { + _stop = true; + Monitor.Pulse(this); + } + break; + } + else + _unrar.Skip(); + } + } + //catch (StopExtractionException) + //{ + //} + } + } +} diff --git a/UnRarDotNet/UnRarDotNet.csproj b/UnRarDotNet/UnRarDotNet.csproj new file mode 100644 index 0000000..a15809d --- /dev/null +++ b/UnRarDotNet/UnRarDotNet.csproj @@ -0,0 +1,92 @@ + + + Debug + AnyCPU + 8.0.50727 + 2.0 + {8427CAA5-80B8-4952-9A68-5F3DFCFBDF40} + Library + Properties + UnRarDotNet + UnRarDotNet + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + ..\bin\x64\Debug\ + DEBUG;TRACE + full + x64 + C:\Program Files (x86)\Microsoft Visual Studio 8\Team Tools\Static Analysis Tools\FxCop\\rules + true + GlobalSuppressions.cs + prompt + + + ..\bin\x64\Release\ + TRACE + true + pdbonly + x64 + C:\Program Files (x86)\Microsoft Visual Studio 8\Team Tools\Static Analysis Tools\FxCop\\rules + true + GlobalSuppressions.cs + prompt + + + true + ..\bin\Win32\Debug\ + DEBUG;TRACE + full + x86 + C:\Program Files (x86)\Microsoft Visual Studio 8\Team Tools\Static Analysis Tools\FxCop\\rules + true + GlobalSuppressions.cs + prompt + + + ..\bin\Win32\Release\ + TRACE + true + pdbonly + x86 + C:\Program Files (x86)\Microsoft Visual Studio 8\Team Tools\Static Analysis Tools\FxCop\\rules + true + GlobalSuppressions.cs + prompt + + + + + + + + + + + + + + \ No newline at end of file diff --git a/UnRarDotNet/Unrar.cs b/UnRarDotNet/Unrar.cs new file mode 100644 index 0000000..a670de3 --- /dev/null +++ b/UnRarDotNet/Unrar.cs @@ -0,0 +1,969 @@ +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