using System; using System.Collections.Generic; using System.IO; using System.Linq; using SabreTools.Library.Data; using SabreTools.Library.DatFiles; using SabreTools.Library.DatItems; using SabreTools.Library.IO; using SharpCompress.Archives; using SharpCompress.Archives.Rar; using SharpCompress.Readers; namespace SabreTools.Library.FileTypes { /// /// Represents a TorrentRAR archive for reading and writing /// public class RarArchive : BaseArchive { #region Constructors /// /// Create a new TorrentRARArchive with no base file /// public RarArchive() : base() { this.Type = FileType.RarArchive; } /// /// Create a new TorrentRARArchive from the given file /// /// Name of the file to use as an archive /// True for opening file as read, false for opening file as write /// True if hashes for this file should be calculated, false otherwise (default) public RarArchive(string filename, bool getHashes = false) : base(filename, getHashes) { this.Type = FileType.RarArchive; } #endregion #region Extraction /// /// Attempt to extract a file as an archive /// /// Output directory for archive extraction /// True if the extraction was a success, false otherwise public override bool CopyAll(string outDir) { bool encounteredErrors = true; try { // Create the temp directory Directory.CreateDirectory(outDir); // Extract all files to the temp directory SharpCompress.Archives.Rar.RarArchive ra = SharpCompress.Archives.Rar.RarArchive.Open(this.Filename); foreach (RarArchiveEntry entry in ra.Entries) { entry.WriteToDirectory(outDir, new SharpCompress.Common.ExtractionOptions { PreserveFileTime = true, ExtractFullPath = true, Overwrite = true }); } encounteredErrors = false; ra.Dispose(); } catch (EndOfStreamException) { // Catch this but don't count it as an error because SharpCompress is unsafe } catch (InvalidOperationException) { encounteredErrors = true; } catch (Exception ex) { Globals.Logger.Error(ex.ToString()); encounteredErrors = true; } return encounteredErrors; } /// /// Attempt to extract a file from an archive /// /// Name of the entry to be extracted /// Output directory for archive extraction /// Name of the extracted file, null on error public override string CopyToFile(string entryName, string outDir) { // Try to extract a stream using the given information (MemoryStream ms, string realEntry) = CopyToStream(entryName); // If the memory stream and the entry name are both non-null, we write to file if (ms != null && realEntry != null) { realEntry = Path.Combine(outDir, realEntry); // Create the output subfolder now Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); // Now open and write the file if possible FileStream fs = FileExtensions.TryCreate(realEntry); if (fs != null) { ms.Seek(0, SeekOrigin.Begin); byte[] zbuffer = new byte[_bufferSize]; int zlen; while ((zlen = ms.Read(zbuffer, 0, _bufferSize)) > 0) { fs.Write(zbuffer, 0, zlen); fs.Flush(); } ms?.Dispose(); fs?.Dispose(); } else { ms?.Dispose(); fs?.Dispose(); realEntry = null; } } return realEntry; } /// /// Attempt to extract a stream from an archive /// /// Name of the entry to be extracted /// Output representing the entry name that was found /// MemoryStream representing the entry, null on error public override (MemoryStream, string) CopyToStream(string entryName) { MemoryStream ms = new MemoryStream(); string realEntry = null; try { SharpCompress.Archives.Rar.RarArchive ra = SharpCompress.Archives.Rar.RarArchive.Open(this.Filename, new ReaderOptions { LeaveStreamOpen = false, }); foreach (RarArchiveEntry entry in ra.Entries) { if (entry != null && !entry.IsDirectory && entry.Key.Contains(entryName)) { // Write the file out realEntry = entry.Key; entry.WriteTo(ms); } } ra.Dispose(); } catch (Exception ex) { Globals.Logger.Error(ex.ToString()); ms = null; realEntry = null; } return (ms, realEntry); } #endregion #region Information /// /// Generate a list of DatItem objects from the header values in an archive /// /// Hash representing the hashes that should be skipped /// True if entry dates should be included, false otherwise (default) /// List of DatItem objects representing the found data /// TODO: All instances of Hash.DeepHashes should be made into 0x0 eventually public override List GetChildren(Hash omitFromScan = Hash.DeepHashes, bool date = false) { List found = new List(); string gamename = Path.GetFileNameWithoutExtension(this.Filename); try { SharpCompress.Archives.Rar.RarArchive ra = SharpCompress.Archives.Rar.RarArchive.Open(FileExtensions.TryOpenRead(this.Filename)); foreach (RarArchiveEntry entry in ra.Entries.Where(e => e != null && !e.IsDirectory)) { // If secure hashes are disabled, do a quickscan if (omitFromScan == Hash.SecureHashes) { found.Add(new BaseFile { Filename = entry.Key, Size = entry.Size, CRC = BitConverter.GetBytes(entry.Crc), Date = (date && entry.LastModifiedTime != null ? entry.LastModifiedTime?.ToString("yyyy/MM/dd hh:mm:ss") : null), Parent = gamename, }); } // Otherwise, use the stream directly else { Stream entryStream = entry.OpenEntryStream(); BaseFile rarEntryRom = entryStream.GetInfo(entry.Size, omitFromScan); rarEntryRom.Filename = entry.Key; rarEntryRom.Parent = gamename; rarEntryRom.Date = entry.LastModifiedTime?.ToString("yyyy/MM/dd hh:mm:ss"); found.Add(rarEntryRom); entryStream.Dispose(); } } // Dispose of the archive ra.Dispose(); } catch (Exception ex) { Globals.Logger.Error(ex.ToString()); return null; } return found; } /// /// Generate a list of empty folders in an archive /// /// Input file to get data from /// List of empty folders in the archive public override List GetEmptyFolders() { List empties = new List(); try { SharpCompress.Archives.Rar.RarArchive ra = SharpCompress.Archives.Rar.RarArchive.Open(this.Filename, new ReaderOptions { LeaveStreamOpen = false }); List rarEntries = ra.Entries.OrderBy(e => e.Key, new NaturalSort.NaturalReversedComparer()).ToList(); string lastRarEntry = null; foreach (RarArchiveEntry entry in rarEntries) { if (entry != null) { // If the current is a superset of last, we skip it if (lastRarEntry != null && lastRarEntry.StartsWith(entry.Key)) { // No-op } // If the entry is a directory, we add it else if (entry.IsDirectory) { empties.Add(entry.Key); lastRarEntry = entry.Key; } } } } catch (Exception ex) { Globals.Logger.Error(ex.ToString()); } return empties; } /// /// Check whether the input file is a standardized format /// public override bool IsTorrent() { throw new NotImplementedException(); } #endregion #region Writing /// /// Write an input file to a torrentrar archive /// /// Input filename to be moved /// Output directory to build to /// DatItem representing the new information /// True if the date from the DAT should be used if available, false otherwise (default) /// Positive value for depth of the output depot, defaults to 4 /// True if the archive was written properly, false otherwise public override bool Write(string inputFile, string outDir, Rom rom, bool date = false, int depth = 4) { // Get the file stream for the file and write out return Write(FileExtensions.TryOpenRead(inputFile), outDir, rom, date, depth); } /// /// Write an input stream to a torrentrar archive /// /// Input stream to be moved /// Output directory to build to /// DatItem representing the new information /// True if the date from the DAT should be used if available, false otherwise (default) /// Positive value for depth of the output depot, defaults to 4 /// True if the archive was written properly, false otherwise public override bool Write(Stream inputStream, string outDir, Rom rom, bool date = false, int depth = 4) { throw new NotImplementedException(); } /// /// Write a set of input files to a torrentrar archive (assuming the same output archive name) /// /// Input files to be moved /// Output directory to build to /// DatItem representing the new information /// True if the date from the DAT should be used if available, false otherwise (default) /// True if files should be output in Romba depot folders, false otherwise /// True if the archive was written properly, false otherwise public override bool Write(List inputFiles, string outDir, List roms, bool date = false, bool romba = false) { throw new NotImplementedException(); } #endregion } }