using System; using System.Collections.Generic; using System.Linq; using SabreTools.Library.Data; using SabreTools.Library.DatItems; using SabreTools.Library.Tools; #if MONO using System.IO; #else using Alphaleonis.Win32.Filesystem; using BinaryReader = System.IO.BinaryReader; using EndOfStreamException = System.IO.EndOfStreamException; using FileStream = System.IO.FileStream; using MemoryStream = System.IO.MemoryStream; using SeekOrigin = System.IO.SeekOrigin; using Stream = System.IO.Stream; #endif using SharpCompress.Archives; using SharpCompress.Archives.Rar; using SharpCompress.Common; 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() { } /// /// 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 public RarArchive(string filename) : base(filename) { _archiveType = ArchiveType.Rar; } #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 ExtractAll(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(_filename); foreach (RarArchiveEntry entry in ra.Entries) { entry.WriteToDirectory(outDir, new 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) { // Don't log file open errors 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 ExtractEntry(string entryName, string outDir) { // Try to extract a stream using the given information (MemoryStream ms, string realEntry) = ExtractEntryStream(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 = Utilities.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) ExtractEntryStream(string entryName) { MemoryStream ms = new MemoryStream(); string realEntry = null; try { SharpCompress.Archives.Rar.RarArchive ra = SharpCompress.Archives.Rar.RarArchive.Open(_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 GetArchiveFileInfo(Hash omitFromScan = Hash.DeepHashes, bool date = false) { List found = new List(); string gamename = Path.GetFileNameWithoutExtension(_filename); try { SharpCompress.Archives.Rar.RarArchive ra = SharpCompress.Archives.Rar.RarArchive.Open(Utilities.TryOpenRead(_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 Rom { Type = ItemType.Rom, Name = entry.Key, Size = entry.Size, CRC = entry.Crc.ToString("X").ToLowerInvariant(), Date = (date && entry.LastModifiedTime != null ? entry.LastModifiedTime?.ToString("yyyy/MM/dd hh:mm:ss") : null), MachineName = gamename, }); } // Otherwise, use the stream directly else { Stream entryStream = entry.OpenEntryStream(); Rom rarEntryRom = (Rom)Utilities.GetStreamInfo(entryStream, entry.Size, omitFromScan: omitFromScan); rarEntryRom.Name = entry.Key; rarEntryRom.MachineName = 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) { // Don't log file open errors return null; } return found; } /// /// (INCOMPLETE) Retrieve file information for a RAR file /// /// TODO: Write the rest of this RAR file handling public void GetRarFileInfo() { if (!File.Exists(_filename)) { return; } BinaryReader br = new BinaryReader(Utilities.TryOpenRead(_filename)); // Check for the signature first (Skipping the SFX Module) byte[] signature = br.ReadBytes(8); int startpos = 0; while (startpos < Constants.MibiByte && BitConverter.ToString(signature, 0, 7) != Constants.RarSig && BitConverter.ToString(signature) != Constants.RarFiveSig) { startpos++; br.BaseStream.Position = startpos; signature = br.ReadBytes(8); } if (BitConverter.ToString(signature, 0, 7) != Constants.RarSig && BitConverter.ToString(signature) != Constants.RarFiveSig) { return; } CoreRarArchive cra = new CoreRarArchive(); if (startpos > 0) { br.BaseStream.Position = 0; cra.SFX = br.ReadBytes(startpos); } // Get all archive header information cra.HeaderCRC32 = br.ReadUInt32(); cra.HeaderSize = br.ReadUInt32(); uint headerType = br.ReadUInt32(); // Special encryption information bool hasEncryptionHeader = false; // If it's encrypted if (headerType == (uint)RarHeaderType.ArchiveEncryption) { hasEncryptionHeader = true; cra.EncryptionHeaderCRC32 = cra.HeaderCRC32; cra.EncryptionHeaderSize = cra.HeaderSize; cra.EncryptionHeaderFlags = (RarHeaderFlags)br.ReadUInt32(); cra.EncryptionVersion = br.ReadUInt32(); cra.EncryptionFlags = br.ReadUInt32(); cra.KDFCount = br.ReadByte(); cra.Salt = br.ReadBytes(16); cra.CheckValue = br.ReadBytes(12); cra.HeaderCRC32 = br.ReadUInt32(); cra.HeaderSize = br.ReadUInt32(); headerType = br.ReadUInt32(); } cra.HeaderFlags = (RarHeaderFlags)br.ReadUInt32(); if ((cra.HeaderFlags & RarHeaderFlags.ExtraAreaPresent) != 0) { cra.ExtraAreaSize = br.ReadUInt32(); } cra.ArchiveFlags = (RarArchiveFlags)br.ReadUInt32(); if ((cra.ArchiveFlags & RarArchiveFlags.VolumeNumberField) != 0) { cra.VolumeNumber = br.ReadUInt32(); } if (((cra.HeaderFlags & RarHeaderFlags.ExtraAreaPresent) != 0) && cra.ExtraAreaSize != 0) { cra.ExtraArea = br.ReadBytes((int)cra.ExtraAreaSize); } // Archive Comment Service Header // Now for file headers for (; ; ) { CoreRarArchiveEntry crae = new CoreRarArchiveEntry(); crae.HeaderCRC32 = br.ReadUInt32(); crae.HeaderSize = br.ReadUInt32(); crae.HeaderType = (RarHeaderType)br.ReadUInt32(); if (crae.HeaderType == RarHeaderType.EndOfArchive) { break; } crae.HeaderFlags = (RarHeaderFlags)br.ReadUInt32(); if ((crae.HeaderFlags & RarHeaderFlags.ExtraAreaPresent) != 0) { crae.ExtraAreaSize = br.ReadUInt32(); } if ((crae.HeaderFlags & RarHeaderFlags.DataAreaPresent) != 0) { crae.DataAreaSize = br.ReadUInt32(); } crae.FileFlags = (RarFileFlags)br.ReadUInt32(); crae.UnpackedSize = br.ReadUInt32(); if ((crae.FileFlags & RarFileFlags.UnpackedSizeUnknown) != 0) { crae.UnpackedSize = 0; } crae.Attributes = br.ReadUInt32(); crae.mtime = br.ReadUInt32(); crae.DataCRC32 = br.ReadUInt32(); crae.CompressionInformation = br.ReadUInt32(); crae.HostOS = br.ReadUInt32(); crae.NameLength = br.ReadUInt32(); crae.Name = br.ReadBytes((int)crae.NameLength); if ((crae.HeaderFlags & RarHeaderFlags.ExtraAreaPresent) != 0) { uint extraSize = br.ReadUInt32(); switch (br.ReadUInt32()) // Extra Area Type { case 0x01: // File encryption information crae.EncryptionSize = extraSize; crae.EncryptionFlags = (RarEncryptionFlags)br.ReadUInt32(); crae.KDFCount = br.ReadByte(); crae.Salt = br.ReadBytes(16); crae.IV = br.ReadBytes(16); crae.CheckValue = br.ReadBytes(12); break; case 0x02: // File data hash crae.HashSize = extraSize; crae.HashType = br.ReadUInt32(); crae.HashData = br.ReadBytes(32); break; case 0x03: // High precision file time crae.TimeSize = extraSize; crae.TimeFlags = (RarTimeFlags)br.ReadUInt32(); if ((crae.TimeFlags & RarTimeFlags.TimeInUnixFormat) != 0) { if ((crae.TimeFlags & RarTimeFlags.ModificationTimePresent) != 0) { crae.TimeMtime64 = br.ReadUInt64(); } if ((crae.TimeFlags & RarTimeFlags.CreationTimePresent) != 0) { crae.TimeCtime64 = br.ReadUInt64(); } if ((crae.TimeFlags & RarTimeFlags.LastAccessTimePresent) != 0) { crae.TimeLtime64 = br.ReadUInt64(); } } else { if ((crae.TimeFlags & RarTimeFlags.ModificationTimePresent) != 0) { crae.TimeMtime = br.ReadUInt32(); } if ((crae.TimeFlags & RarTimeFlags.CreationTimePresent) != 0) { crae.TimeCtime = br.ReadUInt32(); } if ((crae.TimeFlags & RarTimeFlags.LastAccessTimePresent) != 0) { crae.TimeLtime = br.ReadUInt32(); } } break; case 0x04: // File version number crae.VersionSize = extraSize; /* crae.VersionFlags = */ br.ReadUInt32(); crae.VersionNumber = br.ReadUInt32(); break; case 0x05: // File system redirection crae.RedirectionSize = extraSize; crae.RedirectionType = (RarRedirectionType)br.ReadUInt32(); crae.RedirectionFlags = br.ReadUInt32(); crae.RedirectionNameLength = br.ReadUInt32(); crae.RedirectionName = br.ReadBytes((int)crae.RedirectionNameLength); break; case 0x06: // Unix owner and group information crae.UnixOwnerSize = extraSize; crae.UnixOwnerFlags = (RarUnixOwnerRecordFlags)br.ReadUInt32(); if ((crae.UnixOwnerFlags & RarUnixOwnerRecordFlags.UserNameStringIsPresent) != 0) { crae.UnixOwnerUserNameLength = br.ReadUInt32(); crae.UnixOwnerUserName = br.ReadBytes((int)crae.UnixOwnerUserNameLength); } if ((crae.UnixOwnerFlags & RarUnixOwnerRecordFlags.GroupNameStringIsPresent) != 0) { crae.UnixOwnerGroupNameLength = br.ReadUInt32(); crae.UnixOwnerGroupName = br.ReadBytes((int)crae.UnixOwnerGroupNameLength); } if ((crae.UnixOwnerFlags & RarUnixOwnerRecordFlags.NumericUserIdIsPresent) != 0) { crae.UnixOwnerUserId = br.ReadUInt32(); } if ((crae.UnixOwnerFlags & RarUnixOwnerRecordFlags.NumericGroupIdIsPresent) != 0) { crae.UnixOwnerGroupId = br.ReadUInt32(); } break; case 0x07: // Service header data array break; } } if ((crae.HeaderFlags & RarHeaderFlags.DataAreaPresent) != 0) { crae.DataArea = br.ReadBytes((int)crae.DataAreaSize); } } } /// /// 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(_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) /// 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(string inputFile, string outDir, Rom rom, bool date = false, bool romba = false) { // Get the file stream for the file and write out return Write(Utilities.TryOpenRead(inputFile), outDir, rom, date, romba); } /// /// 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) /// 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(Stream inputStream, string outDir, Rom rom, bool date = false, bool romba = false) { 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 } }