diff --git a/SabreTools.Helper/Tools/ArchiveTools.cs b/SabreTools.Helper/Tools/ArchiveTools.cs index 7b8ec6fd..6ce81fb9 100644 --- a/SabreTools.Helper/Tools/ArchiveTools.cs +++ b/SabreTools.Helper/Tools/ArchiveTools.cs @@ -298,172 +298,43 @@ namespace SabreTools.Helper.Tools /// /// Name of the archive to be extracted /// Name of the entry to be extracted - /// Temporary directory for archive extraction + /// Output directory for archive extraction /// Name of the extracted file, null on error - public static string ExtractItem(string input, string entryName, string tempDir) + public static string ExtractItem(string input, string entryName, string outDir) { - string realEntry = ""; + // Try to extract a stream using the given information + (MemoryStream ms, string realEntry) = ExtractStream(input, entryName); - // Set the real entry name - realEntry = ""; - - // First get the archive type - ArchiveType? at = GetCurrentArchiveType(input); - - // If we got back null, then it's not an archive, so we we return - if (at == null) + // If the memory stream and the entry name are both non-null, we write to file + if (ms != null && realEntry != null) { - return realEntry; - } + realEntry = Path.Combine(outDir, realEntry); - try - { - switch (at) + // Create the output subfolder now + Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); + + // Now open and write the file if possible + FileStream fs = FileTools.TryCreate(realEntry); + if (fs != null) { - case ArchiveType.SevenZip: - SevenZipArchive sza = SevenZipArchive.Open(input, new ReaderOptions { LeaveStreamOpen = false, }); - foreach (SevenZipArchiveEntry entry in sza.Entries) - { - if (entry != null && !entry.IsDirectory && entry.Key.Contains(entryName)) - { - realEntry = entry.Key; + 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(); + } - // Get the output path - realEntry = Path.Combine(Path.GetFullPath(tempDir), realEntry); - if (!Directory.Exists(Path.GetDirectoryName(realEntry))) - { - Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); - } - - // Write the file out - entry.WriteToFile(realEntry); - break; - } - } - sza.Dispose(); - break; - - case ArchiveType.GZip: - // Decompress the input stream - realEntry = Path.GetFileNameWithoutExtension(input); - GZipStream gzstream = new GZipStream(FileTools.TryOpenRead(input), Ionic.Zlib.CompressionMode.Decompress); - - // Get the output path - realEntry = Path.Combine(Path.GetFullPath(tempDir), realEntry); - if (!Directory.Exists(Path.GetDirectoryName(realEntry))) - { - Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); - } - - // Write the file out - FileStream gzfileout = FileTools.TryCreate(realEntry); - byte[] gbuffer = new byte[_bufferSize]; - int glen; - while ((glen = gzstream.Read(gbuffer, 0, _bufferSize)) > 0) - { - - gzfileout.Write(gbuffer, 0, glen); - gzfileout.Flush(); - } - - // Dispose of the streams - gzstream.Dispose(); - gzfileout.Dispose(); - break; - - case ArchiveType.Rar: - RarArchive ra = RarArchive.Open(input, new ReaderOptions { LeaveStreamOpen = false, }); - foreach (RarArchiveEntry entry in ra.Entries) - { - if (entry != null && !entry.IsDirectory && entry.Key.Contains(entryName)) - { - realEntry = entry.Key; - - // Get the output path - realEntry = Path.Combine(Path.GetFullPath(tempDir), realEntry); - if (!Directory.Exists(Path.GetDirectoryName(realEntry))) - { - Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); - } - - // Write the file out - entry.WriteToFile(realEntry); - break; - } - } - ra.Dispose(); - break; - - case ArchiveType.Tar: - TarArchive ta = TarArchive.Open(input, new ReaderOptions { LeaveStreamOpen = false, }); - foreach (TarArchiveEntry entry in ta.Entries) - { - if (entry != null && !entry.IsDirectory && entry.Key.Contains(entryName)) - { - realEntry = entry.Key; - - // Get the output path - realEntry = Path.Combine(Path.GetFullPath(tempDir), realEntry); - if (!Directory.Exists(Path.GetDirectoryName(realEntry))) - { - Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); - } - - // Write the file out - entry.WriteToFile(realEntry); - break; - } - } - ta.Dispose(); - break; - - case ArchiveType.Zip: - ZipFile zf = new ZipFile(); - ZipReturn zr = zf.Open(input, new FileInfo(input).LastWriteTime.Ticks, true); - if (zr != ZipReturn.ZipGood) - { - throw new Exception(ZipFile.ZipErrorMessageText(zr)); - } - - for (int i = 0; i < zf.EntriesCount && zr == ZipReturn.ZipGood; i++) - { - if (zf.Entries[i].FileName.Contains(entryName)) - { - realEntry = zf.Entries[i].FileName; - - // Open the read stream - zr = zf.OpenReadStream(i, false, out Stream readStream, out ulong streamsize, out SabreTools.Helper.Data.CompressionMethod cm, out uint lastMod); - - // Get the output path - realEntry = Path.Combine(Path.GetFullPath(tempDir), realEntry); - if (!Directory.Exists(Path.GetDirectoryName(realEntry))) - { - Directory.CreateDirectory(Path.GetDirectoryName(realEntry)); - } - - // Write the file out - FileStream zipfileout = FileTools.TryCreate(realEntry); - byte[] zbuffer = new byte[_bufferSize]; - int zlen; - while ((zlen = readStream.Read(zbuffer, 0, _bufferSize)) > 0) - { - zipfileout.Write(zbuffer, 0, zlen); - zipfileout.Flush(); - } - - zr = zf.CloseReadStream(); - zipfileout.Dispose(); - } - } - - zf.Dispose(); - break; + ms?.Dispose(); + fs?.Dispose(); + } + else + { + ms?.Dispose(); + fs?.Dispose(); + realEntry = null; } - } - catch (Exception ex) - { - Globals.Logger.Error(ex.ToString()); - realEntry = ""; } return realEntry; @@ -474,11 +345,12 @@ namespace SabreTools.Helper.Tools /// /// Name of the archive to be extracted /// Name of the entry to be extracted - /// Temporary directory for archive extraction + /// Output representing the entry name that was found /// MemoryStream representing the entry, null on error - public static MemoryStream ExtractStream(string input, string entryName, string tempDir) + public static (MemoryStream, string) ExtractStream(string input, string entryName) { MemoryStream ms = new MemoryStream(); + string realEntry = null; // First get the archive type ArchiveType? at = GetCurrentArchiveType(input); @@ -486,7 +358,7 @@ namespace SabreTools.Helper.Tools // If we got back null, then it's not an archive, so we we return if (at == null) { - return null; + return (null, realEntry); } try @@ -500,6 +372,7 @@ namespace SabreTools.Helper.Tools if (entry != null && !entry.IsDirectory && entry.Key.Contains(entryName)) { // Write the file out + realEntry = entry.Key; entry.WriteTo(ms); break; } @@ -509,6 +382,7 @@ namespace SabreTools.Helper.Tools case ArchiveType.GZip: // Decompress the input stream + realEntry = Path.GetFileNameWithoutExtension(input); GZipStream gzstream = new GZipStream(FileTools.TryOpenRead(input), Ionic.Zlib.CompressionMode.Decompress); // Write the file out @@ -532,6 +406,7 @@ namespace SabreTools.Helper.Tools if (entry != null && !entry.IsDirectory && entry.Key.Contains(entryName)) { // Write the file out + realEntry = entry.Key; entry.WriteTo(ms); } } @@ -545,6 +420,7 @@ namespace SabreTools.Helper.Tools if (entry != null && !entry.IsDirectory && entry.Key.Contains(entryName)) { // Write the file out + realEntry = entry.Key; entry.WriteTo(ms); } } @@ -564,6 +440,7 @@ namespace SabreTools.Helper.Tools if (zf.Entries[i].FileName.Contains(entryName)) { // Open the read stream + realEntry = zf.Entries[i].FileName; zr = zf.OpenReadStream(i, false, out Stream readStream, out ulong streamsize, out SabreTools.Helper.Data.CompressionMethod cm, out uint lastMod); // Write the file out @@ -587,9 +464,10 @@ namespace SabreTools.Helper.Tools { Globals.Logger.Error(ex.ToString()); ms = null; + realEntry = null; } - return ms; + return (ms, realEntry); } #endregion @@ -1602,11 +1480,143 @@ namespace SabreTools.Helper.Tools /// True if the archive was written properly, false otherwise public static bool WriteTAR(string inputFile, string outDir, Rom rom, bool date = false) { - // Wrap the individual inputs into lists - List inputFiles = new List() { inputFile }; - List roms = new List() { rom }; + // Get the file stream for the file and write out + return WriteTAR(FileTools.TryOpenRead(inputFile), outDir, rom, date: date); + } - return WriteTAR(inputFiles, outDir, roms, date: date); + /// + /// Write an input stream to a tape archive + /// + /// Input stream to be moved + /// Output directory to build to + /// RomData representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if the archive was written properly, false otherwise + public static bool WriteTAR(Stream inputStream, string outDir, Rom rom, bool date = false) + { + bool success = false; + string tempFile = Path.Combine(Path.GetTempPath(), "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; + } + + // Get the output archive name from the first rebuild rom + string archiveFileName = Path.Combine(outDir, Style.RemovePathUnsafeCharacters(rom.Machine.Name) + (rom.Machine.Name.EndsWith(".tar") ? "" : ".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)) + { + // Copy the input stream to the output + tarFile.AddEntry(rom.Name, inputStream); + } + + // 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.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) + { + // Copy the input file to the output + tarFile.AddEntry(rom.Name, inputStream); + } + + // Otherwise, copy the file from the old archive + else + { + // Get the stream from the original archive + string tempEntry = Path.Combine(Path.GetTempPath(), "tmp" + Guid.NewGuid().ToString()); + oldTarFile.Entries.Where(e => e.Key == key).ToList()[0].WriteToFile(tempEntry); + + // Copy the input stream to the output + tarFile.AddEntry(key, tempEntry); + } + } + } + + // Close the output tar file + tarFile.SaveTo(tempFile, new WriterOptions(CompressionType.None)); + + success = true; + } + catch (Exception ex) + { + Console.WriteLine(ex); + success = false; + } + finally + { + inputStream.Dispose(); + tarFile.Dispose(); + oldTarFile.Dispose(); + } + + // If the old file exists, delete it and replace + if (File.Exists(archiveFileName)) + { + FileTools.TryDeleteFile(archiveFileName); + } + File.Move(tempFile, archiveFileName); + + return true; } /// @@ -1784,11 +1794,191 @@ namespace SabreTools.Helper.Tools /// True if the archive was written properly, false otherwise public static bool WriteTorrent7Zip(string inputFile, string outDir, Rom rom, bool date = false) { - // Wrap the individual inputs into lists - List inputFiles = new List() { inputFile }; - List roms = new List() { rom }; + // Get the file stream for the file and write out + return WriteTorrent7Zip(FileTools.TryOpenRead(inputFile), outDir, rom, date: date); + } - return WriteTorrent7Zip(inputFiles, outDir, roms, date: date); + /// + /// Write an input file to a torrent7z archive + /// + /// Input stream to be moved + /// Output directory to build to + /// RomData representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if the archive was written properly, false otherwise + public static bool WriteTorrent7Zip(Stream inputStream, string outDir, Rom rom, bool date = 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; + } + + // Get the output archive name from the first rebuild rom + string archiveFileName = Path.Combine(outDir, Style.RemovePathUnsafeCharacters(rom.Machine.Name) + (rom.Machine.Name.EndsWith(".7z") ? "" : ".7z")); + + // 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.SevenZip, + CompressionLevel = SevenZip.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, archiveFileName); + } + + // Otherwise, sort the input files and write out in the correct order + else + { + // Open the old archive for reading + Stream oldZipFileStream = FileTools.TryOpenRead(archiveFileName); + oldZipFile = new SevenZipExtractor(oldZipFileStream); + + // 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.SevenZip, + CompressionLevel = SevenZip.CompressionLevel.Normal, + }; + Stream zipFileStream = FileTools.TryOpenWrite(tempFile); + + // 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, archiveFileName); + } + + // Otherwise, copy the file from the old archive + else + { + Stream oldZipFileEntryStream = new MemoryStream(); + oldZipFile.ExtractFile(index, oldZipFileEntryStream); + zipFile.CompressFiles(zipFileStream, key); + oldZipFileEntryStream.Dispose(); + } + } + + zipFileStream.Dispose(); + oldZipFile.Dispose(); + } + + 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); + + 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.Dispose(); + + return true; } /// @@ -2000,20 +2190,42 @@ namespace SabreTools.Helper.Tools /// /// Write an input file to a torrent GZ file /// + /// File to write from + /// Directory to write archive to + /// 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 static bool WriteTorrentGZ(string inputFile, string outDir, bool romba) + { + // Check that the input file exists + if (!File.Exists(inputFile)) + { + Globals.Logger.Warning("File " + inputFile + " does not exist!"); + return false; + } + inputFile = Path.GetFullPath(inputFile); + + // Get the file stream for the file and write out + return WriteTorrentGZ(FileTools.TryOpenRead(inputFile), outDir, romba); + } + + /// + /// Write an input stream to a torrent GZ file + /// /// File to write from /// Directory to write archive to /// 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 static bool WriteTorrentGZ(string input, string outDir, bool romba) + public static bool WriteTorrentGZ(Stream inputStream, string outDir, bool romba) { - // Check that the input file exists - if (!File.Exists(input)) + bool success = false; + + // If the stream is not readable, return + if (!inputStream.CanRead) { - Globals.Logger.Warning("File " + input + " does not exist!"); - return false; + return success; } - input = Path.GetFullPath(input); // Make sure the output directory exists if (!Directory.Exists(outDir)) @@ -2023,7 +2235,7 @@ namespace SabreTools.Helper.Tools outDir = Path.GetFullPath(outDir); // Now get the Rom info for the file so we have hashes and size - Rom rom = FileTools.GetFileInfo(input); + Rom rom = FileTools.GetStreamInfo(inputStream, inputStream.Length, keepReadOpen: true); // Get the output file name string outfile = null; @@ -2049,7 +2261,6 @@ namespace SabreTools.Helper.Tools if (!File.Exists(outfile)) { // Compress the input stream - FileStream inputStream = FileTools.TryOpenRead(input); FileStream outputStream = FileTools.TryCreate(outfile); // Open the output file for writing @@ -2099,24 +2310,21 @@ namespace SabreTools.Helper.Tools /// True if the archive was written properly, false otherwise public static bool WriteTorrentLRZ(string inputFile, string outDir, Rom rom, bool date = false) { - // Wrap the individual inputs into lists - List inputFiles = new List() { inputFile }; - List roms = new List() { rom }; - - return WriteTorrentLRZ(inputFiles, outDir, roms, date: date); + // Get the file stream for the file and write out + return WriteTorrentLRZ(FileTools.TryOpenRead(inputFile), outDir, rom, date: date); } /// - /// (UNIMPLEMENTED) Write a set of input files to a torrentlrzip archive (assuming the same output archive name) + /// Write an input stream to a torrentlrzip archive /// - /// Input filenames to be moved + /// Input stream to be moved /// Output directory to build to - /// List of Rom representing the new information + /// RomData representing the new information /// True if the date from the DAT should be used if available, false otherwise (default) /// True if the archive was written properly, false otherwise - public static bool WriteTorrentLRZ(List inputFiles, string outDir, List roms, bool date = false) + public static bool WriteTorrentLRZ(Stream inputStream, string outDir, Rom rom, bool date = false) { - return false; + throw new NotImplementedException(); } /// @@ -2129,24 +2337,21 @@ namespace SabreTools.Helper.Tools /// True if the archive was written properly, false otherwise public static bool WriteTorrentRAR(string inputFile, string outDir, Rom rom, bool date = false) { - // Wrap the individual inputs into lists - List inputFiles = new List() { inputFile }; - List roms = new List() { rom }; - - return WriteTorrentRAR(inputFiles, outDir, roms, date: date); + // Get the file stream for the file and write out + return WriteTorrentRAR(FileTools.TryOpenRead(inputFile), outDir, rom, date: date); } /// - /// (UNIMPLEMENTED) Write a set of input files to a torrentrar archive (assuming the same output archive name) + /// Write an input stream to a torrentrar archive /// - /// Input filenames to be moved + /// Input stream to be moved /// Output directory to build to - /// List of Rom representing the new information + /// RomData representing the new information /// True if the date from the DAT should be used if available, false otherwise (default) /// True if the archive was written properly, false otherwise - public static bool WriteTorrentRAR(List inputFiles, string outDir, List roms, bool date = false) + public static bool WriteTorrentRAR(Stream inputStream, string outDir, Rom rom, bool date = false) { - return false; + throw new NotImplementedException(); } /// @@ -2159,11 +2364,192 @@ namespace SabreTools.Helper.Tools /// True if the archive was written properly, false otherwise public static bool WriteTorrentXZ(string inputFile, string outDir, Rom rom, bool date = false) { - // Wrap the individual inputs into lists - List inputFiles = new List() { inputFile }; - List roms = new List() { rom }; + // Get the file stream for the file and write out + return WriteTorrentXZ(FileTools.TryOpenRead(inputFile), outDir, rom, date: date); + } - return WriteTorrentXZ(inputFiles, outDir, roms, date: date); + /// + /// Write an input file to a torrentxz archive + /// + /// Input stream to be moved + /// Output directory to build to + /// RomData representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if the archive was written properly, false otherwise + public static bool WriteTorrentXZ(Stream inputStream, string outDir, Rom rom, bool date = 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; + } + + // Get the output archive name from the first rebuild rom + string archiveFileName = Path.Combine(outDir, Style.RemovePathUnsafeCharacters(rom.Machine.Name) + (rom.Machine.Name.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 = SevenZip.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, archiveFileName); + } + + // Otherwise, sort the input files and write out in the correct order + else + { + // Open the old archive for reading + Stream oldZipFileStream = FileTools.TryOpenRead(archiveFileName); + oldZipFile = new SevenZipExtractor(oldZipFileStream); + + // 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 = SevenZip.CompressionLevel.Normal, + }; + Stream zipFileStream = FileTools.TryOpenWrite(tempFile); + + // 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, archiveFileName); + } + + // Otherwise, copy the file from the old archive + else + { + Stream oldZipFileEntryStream = new MemoryStream(); + oldZipFile.ExtractFile(index, oldZipFileEntryStream); + zipFile.CompressFiles(zipFileStream, key); + oldZipFileEntryStream.Dispose(); + } + } + + zipFileStream.Dispose(); + oldZipFile.Dispose(); + } + + 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 TXZ + // 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); + + 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.Dispose(); + + return true; } /// @@ -2382,11 +2768,201 @@ namespace SabreTools.Helper.Tools /// True if the archive was written properly, false otherwise public static bool WriteTorrentZip(string inputFile, string outDir, Rom rom, bool date = false) { - // Wrap the individual inputs into lists - List inputFiles = new List() { inputFile }; - List roms = new List() { rom }; + // Get the file stream for the file and write out + return WriteTorrentZip(FileTools.TryOpenRead(inputFile), outDir, rom, date: date); + } - return WriteTorrentZip(inputFiles, outDir, roms, date: date); + /// + /// Write an input stream to a torrentzip archive + /// + /// Input filename to be moved + /// Output directory to build to + /// RomData representing the new information + /// True if the date from the DAT should be used if available, false otherwise (default) + /// True if the archive was written properly, false otherwise + public static bool WriteTorrentZip(Stream inputStream, string outDir, Rom rom, bool date = 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; + } + + // Get the output archive name from the first rebuild rom + string archiveFileName = Path.Combine(outDir, Style.RemovePathUnsafeCharacters(rom.Machine.Name) + (rom.Machine.Name.EndsWith(".zip") ? "" : ".zip")); + + // Set internal variables + Stream writeStream = null; + ZipFile oldZipFile = new ZipFile(); + ZipFile zipFile = new ZipFile(); + ZipReturn zipReturn = ZipReturn.ZipGood; + + 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)) + { + inputStream.Seek(0, SeekOrigin.Begin); + zipReturn = zipFile.Create(tempFile); + + // Open the input file for reading + ulong istreamSize = (ulong)(inputStream.Length); + + DateTime dt = DateTime.Now; + if (date && !String.IsNullOrEmpty(rom.Date) && DateTime.TryParse(rom.Date.Replace('\\', '/'), out dt)) + { + uint msDosDateTime = Style.ConvertDateTimeToMsDosTimeFormat(dt); + zipFile.OpenWriteStream(false, false, rom.Name.Replace('\\', '/'), istreamSize, + SabreTools.Helper.Data.CompressionMethod.Deflated, out writeStream, lastMod: msDosDateTime); + } + else + { + zipFile.OpenWriteStream(false, true, rom.Name.Replace('\\', '/'), istreamSize, SabreTools.Helper.Data.CompressionMethod.Deflated, out writeStream); + } + + // Copy the input stream to the output + byte[] ibuffer = new byte[_bufferSize]; + int ilen; + while ((ilen = inputStream.Read(ibuffer, 0, _bufferSize)) > 0) + { + writeStream.Write(ibuffer, 0, ilen); + writeStream.Flush(); + } + inputStream.Dispose(); + zipFile.CloseWriteStream(Convert.ToUInt32(rom.CRC, 16)); + } + + // Otherwise, sort the input files and write out in the correct order + else + { + // Open the old archive for reading + oldZipFile.Open(archiveFileName, new FileInfo(archiveFileName).LastWriteTime.Ticks, true); + + // Map all inputs to index + Dictionary inputIndexMap = new Dictionary(); + + // If the old one doesn't contain the new file, then add it + if (oldZipFile.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.EntriesCount; i++) + { + inputIndexMap.Add(oldZipFile.Filename(i), i); + } + + // If the number of entries is the same as the old archive, skip out + if (inputIndexMap.Keys.Count <= oldZipFile.EntriesCount) + { + success = true; + return success; + } + + // Otherwise, process the old zipfile + zipFile.Create(tempFile); + + // 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) + { + // Open the input file for reading + ulong istreamSize = (ulong)(inputStream.Length); + + DateTime dt = DateTime.Now; + if (date && !String.IsNullOrEmpty(rom.Date) && DateTime.TryParse(rom.Date.Replace('\\', '/'), out dt)) + { + uint msDosDateTime = Style.ConvertDateTimeToMsDosTimeFormat(dt); + zipFile.OpenWriteStream(false, false, rom.Name.Replace('\\', '/'), istreamSize, + SabreTools.Helper.Data.CompressionMethod.Deflated, out writeStream, lastMod: msDosDateTime); + } + else + { + zipFile.OpenWriteStream(false, true, rom.Name.Replace('\\', '/'), istreamSize, SabreTools.Helper.Data.CompressionMethod.Deflated, out writeStream); + } + + // Copy the input stream to the output + byte[] ibuffer = new byte[_bufferSize]; + int ilen; + while ((ilen = inputStream.Read(ibuffer, 0, _bufferSize)) > 0) + { + writeStream.Write(ibuffer, 0, ilen); + writeStream.Flush(); + } + inputStream.Dispose(); + zipFile.CloseWriteStream(Convert.ToUInt32(rom.CRC, 16)); + } + + // Otherwise, copy the file from the old archive + else + { + // Instantiate the streams + oldZipFile.OpenReadStream(index, false, out Stream zreadStream, out ulong istreamSize, out SabreTools.Helper.Data.CompressionMethod icompressionMethod, out uint lastMod); + zipFile.OpenWriteStream(false, lastMod == Constants.TorrentZipFileDateTime, oldZipFile.Filename(index), + istreamSize, SabreTools.Helper.Data.CompressionMethod.Deflated, out writeStream, lastMod: lastMod); + + // Copy the input stream to the output + byte[] ibuffer = new byte[_bufferSize]; + int ilen; + while ((ilen = zreadStream.Read(ibuffer, 0, _bufferSize)) > 0) + { + writeStream.Write(ibuffer, 0, ilen); + writeStream.Flush(); + } + zipFile.CloseWriteStream(BitConverter.ToUInt32(oldZipFile.CRC32(index), 0)); + } + } + } + + // Close the output zip file + zipFile.Close(); + + success = true; + } + catch (Exception ex) + { + Console.WriteLine(ex); + success = false; + } + finally + { + zipFile.Dispose(); + oldZipFile.Dispose(); + } + + // If the old file exists, delete it and replace + if (File.Exists(archiveFileName)) + { + FileTools.TryDeleteFile(archiveFileName); + } + File.Move(tempFile, archiveFileName); + + return true; } ///