using System; using System.Collections.Generic; using System.IO; using System.Linq; using SabreTools.Data; using SabreTools.Library.DatItems; using SabreTools.Library.IO; using SabreTools.Library.Tools; using Compress.ZipFile; using SharpCompress.Archives; using SharpCompress.Archives.Tar; using SharpCompress.Common; using SharpCompress.Readers; using SharpCompress.Writers; namespace SabreTools.Library.FileTypes { /// /// Represents a Torrent7zip archive for reading and writing /// /// TODO: Don't try to read entries to MemoryStream during write public class TapeArchive : BaseArchive { #region Constructors /// /// Create a new TorrentTarArchive with no base file /// public TapeArchive() : base() { this.Type = FileType.TapeArchive; } /// /// Create a new TorrentTarArchive 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 TapeArchive(string filename, bool getHashes = false) : base(filename, getHashes) { this.Type = FileType.TapeArchive; } #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 TarArchive ta = TarArchive.Open(this.Filename); foreach (TarArchiveEntry entry in ta.Entries) { entry.WriteToDirectory(outDir, new ExtractionOptions { PreserveFileTime = true, ExtractFullPath = true, Overwrite = true }); } encounteredErrors = false; ta.Dispose(); } catch (EndOfStreamException ex) { // Catch this but don't count it as an error because SharpCompress is unsafe logger.Verbose(ex); } catch (InvalidOperationException ex) { logger.Warning(ex); encounteredErrors = true; } catch (Exception ex) { logger.Error(ex); 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 { TarArchive ta = TarArchive.Open(this.Filename, new ReaderOptions { LeaveStreamOpen = false, }); foreach (TarArchiveEntry entry in ta.Entries) { if (entry != null && !entry.IsDirectory && entry.Key.Contains(entryName)) { // Write the file out realEntry = entry.Key; entry.WriteTo(ms); } } ta.Dispose(); } catch (Exception ex) { logger.Error(ex); ms = null; realEntry = null; } return (ms, realEntry); } #endregion #region Information /// /// Generate a list of DatItem objects from the header values in an archive /// /// List of DatItem objects representing the found data public override List GetChildren() { List found = new List(); string gamename = Path.GetFileNameWithoutExtension(this.Filename); try { TarArchive ta = TarArchive.Open(FileExtensions.TryOpenRead(this.Filename)); foreach (TarArchiveEntry entry in ta.Entries.Where(e => e != null && !e.IsDirectory)) { // Create a blank item for the entry BaseFile tarEntryRom = new BaseFile(); // Perform a quickscan, if flagged to if (this.AvailableHashes == Hash.CRC) { tarEntryRom.Size = entry.Size; tarEntryRom.CRC = BitConverter.GetBytes(entry.Crc); } // Otherwise, use the stream directly else { using (Stream entryStream = entry.OpenEntryStream()) { tarEntryRom = GetInfo(entryStream, size: entry.Size, hashes: this.AvailableHashes); } } // Fill in comon details and add to the list tarEntryRom.Filename = entry.Key; tarEntryRom.Parent = gamename; tarEntryRom.Date = entry.LastModifiedTime?.ToString("yyyy/MM/dd hh:mm:ss"); found.Add(tarEntryRom); } // Dispose of the archive ta.Dispose(); } catch (Exception ex) { logger.Error(ex); 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 { TarArchive ta = TarArchive.Open(this.Filename, new ReaderOptions { LeaveStreamOpen = false }); List tarEntries = ta.Entries.OrderBy(e => e.Key, new NaturalSort.NaturalReversedComparer()).ToList(); string lastTarEntry = null; foreach (TarArchiveEntry entry in tarEntries) { if (entry != null) { // If the current is a superset of last, we skip it if (lastTarEntry != null && lastTarEntry.StartsWith(entry.Key)) { // No-op } // If the entry is a directory, we add it else if (entry.IsDirectory) { empties.Add(entry.Key); lastTarEntry = entry.Key; } } } } catch (Exception ex) { logger.Error(ex); } 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 tape archive /// /// Input filename to be moved /// Output directory to build to /// DatItem representing the new information /// True if the archive was written properly, false otherwise public override bool Write(string inputFile, string outDir, Rom rom) { // Get the file stream for the file and write out return Write(FileExtensions.TryOpenRead(inputFile), outDir, rom); } /// /// Write an input stream to a tape archive /// /// Input stream to be moved /// Output directory to build to /// DatItem representing the new information /// True if the archive was written properly, false otherwise public override bool Write(Stream inputStream, string outDir, Rom rom) { bool success = false; string tempFile = Path.Combine(outDir, $"tmp{Guid.NewGuid()}"); // If either input is null or empty, return if (inputStream == null || rom == null || rom.Name == null) return success; // If the stream is not readable, return if (!inputStream.CanRead) return success; // Get the output archive name from the first rebuild rom string archiveFileName = Path.Combine(outDir, Sanitizer.RemovePathUnsafeCharacters(rom.Machine.Name) + (rom.Machine.Name.EndsWith(".tar") ? string.Empty : ".tar")); // Set internal variables TarArchive oldTarFile = TarArchive.Create(); TarArchive tarFile = TarArchive.Create(); try { // If the full output path doesn't exist, create it if (!Directory.Exists(Path.GetDirectoryName(archiveFileName))) Directory.CreateDirectory(Path.GetDirectoryName(archiveFileName)); // If the archive doesn't exist, create it and put the single file if (!File.Exists(archiveFileName)) { // Get temporary date-time if possible DateTime? usableDate = null; if (UseDates && !string.IsNullOrWhiteSpace(rom.Date) && DateTime.TryParse(rom.Date.Replace('\\', '/'), out DateTime dt)) usableDate = dt; // Copy the input stream to the output inputStream.Seek(0, SeekOrigin.Begin); tarFile.AddEntry(rom.Name, inputStream, size: rom.Size ?? 0, modified: usableDate); } // Otherwise, sort the input files and write out in the correct order else { // Open the old archive for reading oldTarFile = TarArchive.Open(archiveFileName); // Get a list of all current entries List entries = oldTarFile.Entries.Select(i => i.Key).ToList(); // Map all inputs to index Dictionary inputIndexMap = new Dictionary(); // If the old one doesn't contain the new file, then add it if (!entries.Contains(rom.Name.Replace('\\', '/'))) inputIndexMap.Add(rom.Name.Replace('\\', '/'), -1); // Then add all of the old entries to it too for (int i = 0; i < entries.Count; i++) { inputIndexMap.Add(entries[i], i); } // If the number of entries is the same as the old archive, skip out if (inputIndexMap.Keys.Count <= entries.Count) { success = true; return success; } // Get the order for the entries with the new file List keys = inputIndexMap.Keys.ToList(); keys.Sort(ZipFile.TrrntZipStringCompare); // Copy over all files to the new archive foreach (string key in keys) { // Get the index mapped to the key int index = inputIndexMap[key]; // Get temporary date-time if possible DateTime? usableDate = null; if (UseDates && !string.IsNullOrWhiteSpace(rom.Date) && DateTime.TryParse(rom.Date.Replace('\\', '/'), out DateTime dt)) usableDate = dt; // If we have the input file, add it now if (index < 0) { // Copy the input file to the output inputStream.Seek(0, SeekOrigin.Begin); tarFile.AddEntry(rom.Name, inputStream, size: rom.Size ?? 0, modified: usableDate); } // Otherwise, copy the file from the old archive else { // Get the stream from the original archive TarArchiveEntry tae = oldTarFile.Entries.ElementAt(index); MemoryStream entry = new MemoryStream(); tae.OpenEntryStream().CopyTo(entry); // Copy the input stream to the output tarFile.AddEntry(key, entry, size: tae.Size, modified: tae.LastModifiedTime); } } } // Close the output tar file tarFile.SaveTo(tempFile, new WriterOptions(CompressionType.None)); success = true; } catch (Exception ex) { logger.Error(ex); success = false; } finally { tarFile.Dispose(); oldTarFile.Dispose(); } // If the old file exists, delete it and replace if (File.Exists(archiveFileName)) FileExtensions.TryDelete(archiveFileName); File.Move(tempFile, archiveFileName); return success; } /// /// Write a set of input files to a tape 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 archive was written properly, false otherwise public override bool Write(List inputFiles, string outDir, List roms) { bool success = false; string tempFile = Path.Combine(outDir, $"tmp{Guid.NewGuid()}"); // If either list of roms is null or empty, return if (inputFiles == null || roms == null || inputFiles.Count == 0 || roms.Count == 0) { return success; } // If the number of inputs is less than the number of available roms, return if (inputFiles.Count < roms.Count) { return success; } // If one of the files doesn't exist, return foreach (string file in inputFiles) { if (!File.Exists(file)) { return success; } } // Get the output archive name from the first rebuild rom string archiveFileName = Path.Combine(outDir, Sanitizer.RemovePathUnsafeCharacters(roms[0].Machine.Name) + (roms[0].Machine.Name.EndsWith(".tar") ? string.Empty : ".tar")); // Set internal variables TarArchive oldTarFile = TarArchive.Create(); TarArchive tarFile = TarArchive.Create(); try { // If the full output path doesn't exist, create it if (!Directory.Exists(Path.GetDirectoryName(archiveFileName))) { Directory.CreateDirectory(Path.GetDirectoryName(archiveFileName)); } // If the archive doesn't exist, create it and put the single file if (!File.Exists(archiveFileName)) { // Map all inputs to index Dictionary inputIndexMap = new Dictionary(); for (int i = 0; i < inputFiles.Count; i++) { inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), i); } // Sort the keys in TZIP order List keys = inputIndexMap.Keys.ToList(); keys.Sort(ZipFile.TrrntZipStringCompare); // Now add all of the files in order foreach (string key in keys) { // Get the index mapped to the key int index = inputIndexMap[key]; // Get temporary date-time if possible DateTime? usableDate = null; if (UseDates && !string.IsNullOrWhiteSpace(roms[index].Date) && DateTime.TryParse(roms[index].Date.Replace('\\', '/'), out DateTime dt)) usableDate = dt; // Copy the input stream to the output tarFile.AddEntry(roms[index].Name, FileExtensions.TryOpenRead(inputFiles[index]), size: roms[index].Size ?? 0, modified: usableDate); } } // Otherwise, sort the input files and write out in the correct order else { // Open the old archive for reading oldTarFile = TarArchive.Open(archiveFileName); // Get a list of all current entries List entries = oldTarFile.Entries.Select(i => i.Key).ToList(); // Map all inputs to index Dictionary inputIndexMap = new Dictionary(); for (int i = 0; i < inputFiles.Count; i++) { // If the old one contains the new file, then just skip out if (entries.Contains(roms[i].Name.Replace('\\', '/'))) { continue; } inputIndexMap.Add(roms[i].Name.Replace('\\', '/'), -(i + 1)); } // Then add all of the old entries to it too for (int i = 0; i < entries.Count; i++) { inputIndexMap.Add(entries[i], i); } // If the number of entries is the same as the old archive, skip out if (inputIndexMap.Keys.Count <= entries.Count) { success = true; return success; } // Get the order for the entries with the new file List keys = inputIndexMap.Keys.ToList(); keys.Sort(ZipFile.TrrntZipStringCompare); // Copy over all files to the new archive foreach (string key in keys) { // Get the index mapped to the key int index = inputIndexMap[key]; // If we have the input file, add it now if (index < 0) { // Get temporary date-time if possible DateTime? usableDate = null; if (UseDates && !string.IsNullOrWhiteSpace(roms[-index - 1].Date) && DateTime.TryParse(roms[-index - 1].Date.Replace('\\', '/'), out DateTime dt)) usableDate = dt; // Copy the input file to the output tarFile.AddEntry(roms[-index - 1].Name, FileExtensions.TryOpenRead(inputFiles[-index - 1]), size: roms[-index - 1].Size ?? 0, modified: usableDate); } // Otherwise, copy the file from the old archive else { // Get the stream from the original archive TarArchiveEntry tae = oldTarFile.Entries.ElementAt(index); MemoryStream entry = new MemoryStream(); tae.OpenEntryStream().CopyTo(entry); // Copy the input stream to the output tarFile.AddEntry(key, entry, size: tae.Size, modified: tae.LastModifiedTime); } } } // Close the output tar file tarFile.SaveTo(tempFile, new WriterOptions(CompressionType.None)); success = true; } catch (Exception ex) { logger.Error(ex); success = false; } finally { tarFile.Dispose(); oldTarFile.Dispose(); } // If the old file exists, delete it and replace if (File.Exists(archiveFileName)) { FileExtensions.TryDelete(archiveFileName); } File.Move(tempFile, archiveFileName); return true; } #endregion } }