using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; #if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER using SharpCompress.Archives; using SharpCompress.Archives.Rar; using SharpCompress.Common; using SharpCompress.Readers; #endif #pragma warning disable SYSLIB1045 // Convert to 'GeneratedRegexAttribute' namespace SabreTools.Wrappers { /// /// This is a shell wrapper; one that does not contain /// any actual parsing. It is used as a placeholder for /// types that typically do not have models. /// public partial class RAR : IExtractable { /// public bool Extract(string outputDirectory, bool includeDebug) => Extract(outputDirectory, lookForHeader: false, includeDebug); /// public bool Extract(string outputDirectory, bool lookForHeader, bool includeDebug) { if (_dataSource is null || !_dataSource.CanRead) return false; #if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER try { var readerOptions = new ReaderOptions() { LookForHeader = lookForHeader }; var rarFile = (RarArchive)RarArchive.OpenArchive(_dataSource, readerOptions); // If the file exists if (!string.IsNullOrEmpty(Filename) && File.Exists(Filename!)) { // Find all file parts FileInfo[] parts = [.. ArchiveFactory.GetFileParts(new FileInfo(Filename))]; // If there are multiple parts if (parts.Length > 1) rarFile = (RarArchive)RarArchive.OpenArchive(parts, readerOptions); // Try to read the file path if no entries are found else if (rarFile.Entries.Count == 0) rarFile = (RarArchive)RarArchive.OpenArchive(parts, readerOptions); } // Explained in https://github.com/adamhathcock/sharpcompress/pull/661. in order to determine whether // a RAR or 7Z archive is solid or not, you must check the second file in the archive, as the first // file is always marked non-solid even for solid archives. This iteration is necessary since things // like directories aren't marked solid either. // This is only temporary, as solid detection has been fixed in upstream SolidCompress, but they likely // won't make a new release for a while, and this is too big an issue to leave unfixed. bool firstFile = true; bool isSolid = false; foreach (var entry in rarFile.Entries) { try { // If the entry is a directory if (entry.IsDirectory) continue; // If the entry has an invalid key if (entry.Key is null) continue; // If we have a partial entry due to an incomplete multi-part archive, skip it if (!entry.IsComplete) continue; if (firstFile) { firstFile = false; continue; } if (entry.IsSolid) { // If the RAR is solid and the first entry is password-protected, you won't be able to // extract the rest of the entries anyway, so just return early. if (entry.IsEncrypted) { if (includeDebug) Console.WriteLine("RAR is password-protected!"); return false; } isSolid = true; } break; } catch (Exception ex) { if (includeDebug) Console.WriteLine(ex); } } // Still check SharpCompress's archive-level isSolid flag. Based on the way the check works on their // end, it's possible that a multi-block solid archive might have the second (or more files) file not // solid? It doesn't hurt to check either way. if (isSolid || rarFile.IsSolid) return ExtractSolid(rarFile, outputDirectory, includeDebug); else return ExtractNonSolid(rarFile, outputDirectory, includeDebug); } catch (Exception ex) { if (includeDebug) Console.Error.WriteLine(ex); return false; } #else Console.WriteLine("Extraction is not supported for this framework!"); Console.WriteLine(); return false; #endif } /// /// Try to find all parts of the archive, if possible /// /// Path of the first archive part /// List of all found parts, if possible public static List FindParts(string firstPart) { // Define the regex patterns const string rarNewPattern = @"^(.*\.part)([0-9]+)(\.rar)$"; const string rarOldPattern = @"^(.*\.)([r-z{])(ar|[0-9]+)$"; const string genericPattern = @"^(.*\.)([0-9]+)$"; // Ensure the full path is available firstPart = Path.GetFullPath(firstPart); string filename = Path.GetFileName(firstPart); string? directory = Path.GetDirectoryName(firstPart); // Make the output list List parts = []; // Determine which pattern is being used Match match; Func nextPartFunc; if (Regex.IsMatch(filename, rarNewPattern, RegexOptions.IgnoreCase)) { match = Regex.Match(filename, rarNewPattern, RegexOptions.IgnoreCase); nextPartFunc = (i) => { return string.Concat( match.Groups[1].Value, $"{i + 1}".PadLeft(match.Groups[2].Value.Length, '0'), match.Groups[3].Value); }; } else if (Regex.IsMatch(filename, rarOldPattern, RegexOptions.IgnoreCase)) { match = Regex.Match(filename, rarOldPattern, RegexOptions.IgnoreCase); nextPartFunc = (i) => { #if NETCOREAPP || NETSTANDARD2_1_OR_GREATER return string.Concat( match.Groups[1].Value, (char)(match.Groups[2].Value[0] + ((i - 1) / 100)) + (i - 1).ToString("D4")[2..]); #else return string.Concat( match.Groups[1].Value, (char)(match.Groups[2].Value[0] + ((i - 1) / 100)) + (i - 1).ToString("D4").Substring(2)); #endif }; } else if (Regex.IsMatch(filename, genericPattern, RegexOptions.IgnoreCase)) { match = Regex.Match(filename, genericPattern, RegexOptions.IgnoreCase); nextPartFunc = (i) => { return string.Concat( match.Groups[1].Value, $"{i + 1}".PadLeft(match.Groups[2].Value.Length, '0') ); }; } else { return [firstPart]; } // Loop and add the files parts.Add(firstPart); for (int i = 1; ; i++) { string nextPart = nextPartFunc(i); if (directory is not null) nextPart = Path.Combine(directory, nextPart); if (!File.Exists(nextPart)) break; parts.Add(nextPart); } return parts; } #if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER /// /// Extraction method for non-solid archives. This iterates over each entry in the archive to extract every /// file individually, in order to extract all valid files from the archive. /// private static bool ExtractNonSolid(RarArchive rarFile, string outputDirectory, bool includeDebug) { foreach (var entry in rarFile.Entries) { try { // If the entry is a directory if (entry.IsDirectory) continue; // If the entry has an invalid key if (entry.Key is null) continue; // If we have a partial entry due to an incomplete multi-part archive, skip it if (!entry.IsComplete) continue; // If the entry is password-protected, skip it if (entry.IsEncrypted) { if (includeDebug) Console.WriteLine($"File {entry.Key} in RAR is password-protected!"); continue; } // Ensure directory separators are consistent string filename = entry.Key; filename = filename.TrimStart(['\\', '/']); if (Path.DirectorySeparatorChar == '\\') filename = filename.Replace('/', '\\'); else if (Path.DirectorySeparatorChar == '/') filename = filename.Replace('\\', '/'); // Ensure the full output directory exists filename = Path.Combine(outputDirectory, filename); var directoryName = Path.GetDirectoryName(filename); if (directoryName is not null && !Directory.Exists(directoryName)) Directory.CreateDirectory(directoryName); var options = new ExtractionOptions { ExtractFullPath = true, Overwrite = true, }; entry.WriteToFile(filename, options); } catch (Exception ex) { if (includeDebug) Console.Error.WriteLine(ex); } } return true; } /// /// Extraction method for solid archives. Uses ExtractAllEntries because extraction for solid archives must be /// done sequentially, and files beyond a corrupted point in a solid archive will be unreadable anyways. /// private static bool ExtractSolid(RarArchive rarFile, string outputDirectory, bool includeDebug) { try { if (!Directory.Exists(outputDirectory)) Directory.CreateDirectory(outputDirectory); var options = new ExtractionOptions { ExtractFullPath = true, Overwrite = true, }; rarFile.WriteToDirectory(outputDirectory, options); } catch (Exception ex) { if (includeDebug) Console.Error.WriteLine(ex); } return true; } #endif } }