using System; using System.Collections.Generic; using System.Linq; using SabreTools.Library.Data; using SabreTools.Library.Items; using SabreTools.Library.Tools; #if MONO using System.IO; #else using Alphaleonis.Win32.Filesystem; using BinaryWriter = System.IO.BinaryWriter; using FileStream = System.IO.FileStream; using MemoryStream = System.IO.MemoryStream; using SeekOrigin = System.IO.SeekOrigin; using Stream = System.IO.Stream; #endif using ROMVault2.SupportedFiles.Zip; using SevenZip; namespace SabreTools.Library.FileTypes { /// /// Represents a TorrentXZ archive for reading and writing /// /// TODO: Wait for XZ write to be enabled by SevenZipSharp library public class XZArchive : BaseArchive { #region Constructors /// /// Create a new TorrentGZipArchive with no base file /// public XZArchive() : base() { } /// /// Create a new TorrentGZipArchive 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 XZArchive(string filename) : base(filename) { //_archiveType = ArchiveType.XZip; } #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) { throw new NotImplementedException(); } /// /// 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) { throw new NotImplementedException(); } /// /// 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) { throw new NotImplementedException(); } #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) { throw new NotImplementedException(); } /// /// 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() { throw new NotImplementedException(); } #endregion #region Writing /// /// Write an input file to a torrent XZ file /// /// 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 write was a success, false otherwise /// This works for now, but it can be sped up by using Ionic.Zip or another zlib wrapper that allows for header values built-in. See edc's code. 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(FileTools.TryOpenRead(inputFile), outDir, rom, date: date); } /// /// Write an input file to a torrent XZ 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) { bool success = false; string tempFile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString()); // 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; } // Seek to the beginning of the stream inputStream.Seek(0, SeekOrigin.Begin); // Get the output archive name from the first rebuild rom string archiveFileName = Path.Combine(outDir, Style.RemovePathUnsafeCharacters(rom.MachineName) + (rom.MachineName.EndsWith(".xz") ? "" : ".xz")); // Set internal variables SevenZipBase.SetLibraryPath("7za.dll"); SevenZipExtractor oldZipFile = null; SevenZipCompressor zipFile; try { // If the full output path doesn't exist, create it if (!Directory.Exists(Path.GetDirectoryName(tempFile))) { Directory.CreateDirectory(Path.GetDirectoryName(tempFile)); } // If the archive doesn't exist, create it and put the single file if (!File.Exists(archiveFileName)) { zipFile = new SevenZipCompressor() { ArchiveFormat = OutArchiveFormat.XZ, CompressionLevel = CompressionLevel.Normal, }; // Create the temp directory string tempPath = Path.Combine(Path.GetTempPath(), new Guid().ToString()); if (!Directory.Exists(tempPath)) { Directory.CreateDirectory(tempPath); } // Create a stream dictionary Dictionary dict = new Dictionary(); dict.Add(rom.Name, inputStream); // Now add the stream zipFile.CompressStreamDictionary(dict, tempFile); } // Otherwise, sort the input files and write out in the correct order else { // Open the old archive for reading using (oldZipFile = new SevenZipExtractor(archiveFileName)) { // Map all inputs to index Dictionary inputIndexMap = new Dictionary(); // If the old one doesn't contain the new file, then add it if (!oldZipFile.ArchiveFileNames.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 < oldZipFile.FilesCount; i++) { inputIndexMap.Add(oldZipFile.ArchiveFileNames[i], i); } // If the number of entries is the same as the old archive, skip out if (inputIndexMap.Keys.Count <= oldZipFile.FilesCount) { success = true; return success; } // Otherwise, process the old zipfile zipFile = new SevenZipCompressor() { ArchiveFormat = OutArchiveFormat.XZ, CompressionLevel = CompressionLevel.Normal, }; // Get the order for the entries with the new file List keys = inputIndexMap.Keys.ToList(); keys.Sort(ZipFile.TorrentZipStringCompare); // 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) { // Create a stream dictionary Dictionary dict = new Dictionary(); dict.Add(rom.Name, inputStream); // Now add the stream zipFile.CompressStreamDictionary(dict, tempFile); } // Otherwise, copy the file from the old archive else { Stream oldZipFileEntryStream = new MemoryStream(); oldZipFile.ExtractFile(index, oldZipFileEntryStream); oldZipFileEntryStream.Seek(0, SeekOrigin.Begin); // Create a stream dictionary Dictionary dict = new Dictionary(); dict.Add(oldZipFile.ArchiveFileNames[index], oldZipFileEntryStream); // Now add the stream zipFile.CompressStreamDictionary(dict, tempFile); oldZipFileEntryStream.Dispose(); } // After the first file, make sure we're in append mode zipFile.CompressionMode = CompressionMode.Append; } } } success = true; } catch (Exception ex) { Console.WriteLine(ex); success = false; } finally { inputStream?.Dispose(); } // If the old file exists, delete it and replace if (File.Exists(archiveFileName)) { FileTools.TryDeleteFile(archiveFileName); } File.Move(tempFile, archiveFileName); // Now make the file T7Z // TODO: Add ACTUAL T7Z compatible code BinaryWriter bw = new BinaryWriter(FileTools.TryOpenReadWrite(archiveFileName)); bw.Seek(0, SeekOrigin.Begin); bw.Write(Constants.Torrent7ZipHeader); bw.Seek(0, SeekOrigin.End); using (oldZipFile = new SevenZipExtractor(FileTools.TryOpenReadWrite(archiveFileName))) { // Get the correct signature to use (Default 0, Unicode 1, SingleFile 2, StripFileNames 4) byte[] tempsig = Constants.Torrent7ZipSignature; if (oldZipFile.FilesCount > 1) { tempsig[16] = 0x2; } else { tempsig[16] = 0; } bw.Write(tempsig); bw.Flush(); bw.Dispose(); } return true; } /// /// Write a set of input files to a torrent XZ 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) { bool success = false; string tempFile = Path.Combine(outDir, "tmp" + Guid.NewGuid().ToString()); // 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, Style.RemovePathUnsafeCharacters(roms[0].MachineName) + (roms[0].MachineName.EndsWith(".xz") ? "" : ".xz")); // Set internal variables SevenZipBase.SetLibraryPath("7za.dll"); SevenZipExtractor oldZipFile; SevenZipCompressor zipFile; 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)) { zipFile = new SevenZipCompressor() { ArchiveFormat = OutArchiveFormat.XZ, CompressionLevel = CompressionLevel.Normal, }; // 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.TorrentZipStringCompare); // Create the temp directory string tempPath = Path.Combine(Path.GetTempPath(), new Guid().ToString()); if (!Directory.Exists(tempPath)) { Directory.CreateDirectory(tempPath); } // Now add all of the files in order foreach (string key in keys) { string newkey = Path.Combine(tempPath, key); File.Move(inputFiles[inputIndexMap[key]], newkey); zipFile.CompressFiles(tempFile, newkey); File.Move(newkey, inputFiles[inputIndexMap[key]]); // After the first file, make sure we're in append mode zipFile.CompressionMode = CompressionMode.Append; } FileTools.CleanDirectory(tempPath); FileTools.TryDeleteDirectory(tempPath); } // Otherwise, sort the input files and write out in the correct order else { // Open the old archive for reading using (oldZipFile = new SevenZipExtractor(archiveFileName)) { // 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 (oldZipFile.ArchiveFileNames.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 < oldZipFile.FilesCount; i++) { inputIndexMap.Add(oldZipFile.ArchiveFileNames[i], i); } // If the number of entries is the same as the old archive, skip out if (inputIndexMap.Keys.Count <= oldZipFile.FilesCount) { success = true; return success; } // Otherwise, process the old zipfile zipFile = new SevenZipCompressor() { ArchiveFormat = OutArchiveFormat.XZ, CompressionLevel = CompressionLevel.Normal, }; // Get the order for the entries with the new file List keys = inputIndexMap.Keys.ToList(); keys.Sort(ZipFile.TorrentZipStringCompare); // 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) { FileStream inputStream = FileTools.TryOpenRead(inputFiles[-index - 1]); // Create a stream dictionary Dictionary dict = new Dictionary(); dict.Add(key, inputStream); // Now add the stream zipFile.CompressStreamDictionary(dict, tempFile); } // Otherwise, copy the file from the old archive else { Stream oldZipFileEntryStream = new MemoryStream(); oldZipFile.ExtractFile(index, oldZipFileEntryStream); oldZipFileEntryStream.Seek(0, SeekOrigin.Begin); // Create a stream dictionary Dictionary dict = new Dictionary(); dict.Add(oldZipFile.ArchiveFileNames[index], oldZipFileEntryStream); // Now add the stream zipFile.CompressStreamDictionary(dict, tempFile); oldZipFileEntryStream.Dispose(); } // After the first file, make sure we're in append mode zipFile.CompressionMode = CompressionMode.Append; } } } success = true; } catch (Exception ex) { Console.WriteLine(ex); success = false; } // If the old file exists, delete it and replace if (File.Exists(archiveFileName)) { FileTools.TryDeleteFile(archiveFileName); } File.Move(tempFile, archiveFileName); // Now make the file T7Z // TODO: Add ACTUAL T7Z compatible code BinaryWriter bw = new BinaryWriter(FileTools.TryOpenReadWrite(archiveFileName)); bw.Seek(0, SeekOrigin.Begin); bw.Write(Constants.Torrent7ZipHeader); bw.Seek(0, SeekOrigin.End); using (oldZipFile = new SevenZipExtractor(FileTools.TryOpenReadWrite(archiveFileName))) { // Get the correct signature to use (Default 0, Unicode 1, SingleFile 2, StripFileNames 4) byte[] tempsig = Constants.Torrent7ZipSignature; if (oldZipFile.FilesCount > 1) { tempsig[16] = 0x2; } else { tempsig[16] = 0; } bw.Write(tempsig); bw.Flush(); bw.Dispose(); } return true; } #endregion } }