From 7305ad41acfc006cc60ecda10e735bf858c075af Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 27 Jul 2022 16:00:37 +0200 Subject: [PATCH] Allow Penumbra to import regular archives for penumbra mods. --- Penumbra/Import/TexToolsImport.cs | 41 +++---- Penumbra/Import/TexToolsImporter.Archives.cs | 123 +++++++++++++++++++ Penumbra/Import/TexToolsImporter.ModPack.cs | 10 +- Penumbra/Penumbra.csproj | 2 +- Penumbra/UI/Classes/ModFileSystemSelector.cs | 2 +- 5 files changed, 146 insertions(+), 32 deletions(-) create mode 100644 Penumbra/Import/TexToolsImporter.Archives.cs diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index 73cb9029..3dcf5d47 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -1,14 +1,16 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Dalamud.Logging; -using ICSharpCode.SharpZipLib.Zip; using Newtonsoft.Json; using Penumbra.Mods; using FileMode = System.IO.FileMode; +using ZipArchive = SharpCompress.Archives.Zip.ZipArchive; +using ZipArchiveEntry = SharpCompress.Archives.Zip.ZipArchiveEntry; namespace Penumbra.Import; @@ -119,8 +121,13 @@ public partial class TexToolsImporter : IDisposable // Puts out warnings if extension does not correspond to data. private DirectoryInfo VerifyVersionAndImport( FileInfo modPackFile ) { + if( modPackFile.Extension is ".zip" or ".7z" or ".rar" ) + { + return HandleRegularArchive( modPackFile ); + } + using var zfs = modPackFile.OpenRead(); - using var extractedModPack = new ZipFile( zfs ); + using var extractedModPack = ZipArchive.Open( zfs ); var mpl = FindZipEntry( extractedModPack, "TTMPL.mpl" ); if( mpl == null ) @@ -128,7 +135,7 @@ public partial class TexToolsImporter : IDisposable throw new FileNotFoundException( "ZIP does not contain a TTMPL.mpl file." ); } - var modRaw = GetStringFromZipEntry( extractedModPack, mpl, Encoding.UTF8 ); + var modRaw = GetStringFromZipEntry( mpl, Encoding.UTF8 ); // At least a better validation than going by the extension. if( modRaw.Contains( "\"TTMPVersion\":" ) ) @@ -149,30 +156,14 @@ public partial class TexToolsImporter : IDisposable return ImportV1ModPack( modPackFile, extractedModPack, modRaw ); } - // You can in no way rely on any file paths in TTMPs so we need to just do this, sorry - private static ZipEntry? FindZipEntry( ZipFile file, string fileName ) - { - for( var i = 0; i < file.Count; i++ ) - { - var entry = file[ i ]; + private static ZipArchiveEntry? FindZipEntry( ZipArchive file, string fileName ) + => file.Entries.FirstOrDefault( e => !e.IsDirectory && e.Key.Contains( fileName ) ); - if( entry.Name.Contains( fileName ) ) - { - return entry; - } - } - - return null; - } - - private static Stream GetStreamFromZipEntry( ZipFile file, ZipEntry entry ) - => file.GetInputStream( entry ); - - private static string GetStringFromZipEntry( ZipFile file, ZipEntry entry, Encoding encoding ) + private static string GetStringFromZipEntry( ZipArchiveEntry entry, Encoding encoding ) { using var ms = new MemoryStream(); - using var s = GetStreamFromZipEntry( file, entry ); + using var s = entry.OpenEntryStream(); s.CopyTo( ms ); return encoding.GetString( ms.ToArray() ); } @@ -191,7 +182,7 @@ public partial class TexToolsImporter : IDisposable _tmpFileStream = null; } - private StreamDisposer GetSqPackStreamStream( ZipFile file, string entryName ) + private StreamDisposer GetSqPackStreamStream( ZipArchive file, string entryName ) { State = ImporterState.WritingPackToDisk; @@ -202,7 +193,7 @@ public partial class TexToolsImporter : IDisposable throw new FileNotFoundException( $"ZIP does not contain a file named {entryName}." ); } - using var s = file.GetInputStream( entry ); + using var s = entry.OpenEntryStream(); WriteZipEntryToTempFile( s ); diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs new file mode 100644 index 00000000..0f461c79 --- /dev/null +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -0,0 +1,123 @@ +using System; +using System.IO; +using System.Linq; +using Dalamud.Logging; +using Dalamud.Utility; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui.Filesystem; +using Penumbra.Mods; +using SharpCompress.Archives; +using SharpCompress.Common; + +namespace Penumbra.Import; + +public partial class TexToolsImporter +{ + // Extract regular compressed archives that are folders containing penumbra-formatted mods. + // The mod has to either contain a meta.json at top level, or one folder deep. + // If the meta.json is one folder deep, all other files have to be in the same folder. + // The extracted folder gets its name either from that one top-level folder or from the mod name. + // All data is extracted without manipulation of the files or metadata. + private DirectoryInfo HandleRegularArchive( FileInfo modPackFile ) + { + using var zfs = modPackFile.OpenRead(); + using var archive = ArchiveFactory.Open( zfs ); + + var baseName = FindArchiveModMeta( archive, out var leadDir ); + _currentOptionIdx = 0; + _currentNumOptions = 1; + _currentModName = modPackFile.Name; + _currentGroupName = string.Empty; + _currentOptionName = DefaultTexToolsData.Name; + _currentNumFiles = archive.Entries.Count( e => !e.IsDirectory ); + PluginLog.Log( $" -> Importing {archive.Type} Archive." ); + + _currentModDirectory = Mod.CreateModFolder( _baseDirectory, baseName ); + var options = new ExtractionOptions() + { + ExtractFullPath = true, + Overwrite = true, + }; + + State = ImporterState.ExtractingModFiles; + _currentFileIdx = 0; + foreach( var entry in archive.Entries ) + { + _token.ThrowIfCancellationRequested(); + + if( entry.IsDirectory ) + { + ++_currentFileIdx; + continue; + } + + PluginLog.Log( " -> Extracting {0}", entry.Key ); + entry.WriteToDirectory( _currentModDirectory.FullName, options ); + + ++_currentFileIdx; + } + + if( leadDir ) + { + _token.ThrowIfCancellationRequested(); + var oldName = _currentModDirectory.FullName; + var tmpName = oldName + "__tmp"; + Directory.Move( oldName, tmpName ); + Directory.Move( Path.Combine( tmpName, baseName ), oldName ); + Directory.Delete( tmpName ); + _currentModDirectory = new DirectoryInfo( oldName ); + } + + return _currentModDirectory; + } + + // Search the archive for the meta.json file which needs to exist. + private static string FindArchiveModMeta( IArchive archive, out bool leadDir ) + { + var entry = archive.Entries.FirstOrDefault( e => !e.IsDirectory && e.Key.EndsWith( "meta.json" ) ); + // None found. + if( entry == null ) + { + throw new Exception( "Invalid mod archive: No meta.json contained." ); + } + + var ret = string.Empty; + leadDir = false; + + // If the file is not at top-level. + if( entry.Key != "meta.json" ) + { + leadDir = true; + var directory = Path.GetDirectoryName( entry.Key ); + // Should not happen. + if( directory.IsNullOrEmpty() ) + { + throw new Exception( "Invalid mod archive: Unknown error fetching meta.json." ); + } + + ret = directory; + // Check that all other files are also contained in the top-level directory. + if( ret.IndexOfAny( new[] { '/', '\\' } ) >= 0 + || !archive.Entries.All( e => e.Key.StartsWith( ret ) && ( e.Key.Length == ret.Length || e.Key[ ret.Length ] is '/' or '\\' ) ) ) + { + throw new Exception( + "Invalid mod archive: meta.json in wrong location. It needs to be either at root or one directory deep, in which all other files must be nested too." ); + } + } + + // Check that the mod has a valid name in the meta.json file. + using var e = entry.OpenEntryStream(); + using var t = new StreamReader( e ); + using var j = new JsonTextReader( t ); + var obj = JObject.Load( j ); + var name = obj[ nameof( Mod.Name ) ]?.Value< string >().RemoveInvalidPathSymbols() ?? string.Empty; + if( name.Length == 0 ) + { + throw new Exception( "Invalid mod archive: mod meta has no name." ); + } + + // Use either the top-level directory as the mods base name, or the (fixed for path) name in the json. + return ret.Length == 0 ? name : ret; + } +} \ No newline at end of file diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index f15053cf..574efb1f 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -4,10 +4,10 @@ using System.IO; using System.Linq; using System.Text; using Dalamud.Logging; -using ICSharpCode.SharpZipLib.Zip; using Newtonsoft.Json; using Penumbra.Mods; using Penumbra.Util; +using SharpCompress.Archives.Zip; namespace Penumbra.Import; @@ -16,7 +16,7 @@ public partial class TexToolsImporter private DirectoryInfo? _currentModDirectory; // Version 1 mod packs are a simple collection of files without much information. - private DirectoryInfo ImportV1ModPack( FileInfo modPackFile, ZipFile extractedModPack, string modRaw ) + private DirectoryInfo ImportV1ModPack( FileInfo modPackFile, ZipArchive extractedModPack, string modRaw ) { _currentOptionIdx = 0; _currentNumOptions = 1; @@ -46,7 +46,7 @@ public partial class TexToolsImporter } // Version 2 mod packs can either be simple or extended, import accordingly. - private DirectoryInfo ImportV2ModPack( FileInfo _, ZipFile extractedModPack, string modRaw ) + private DirectoryInfo ImportV2ModPack( FileInfo _, ZipArchive extractedModPack, string modRaw ) { var modList = JsonConvert.DeserializeObject< SimpleModPack >( modRaw, JsonSettings )!; @@ -80,7 +80,7 @@ public partial class TexToolsImporter } // Simple V2 mod packs are basically the same as V1 mod packs. - private DirectoryInfo ImportSimpleV2ModPack( ZipFile extractedModPack, SimpleModPack modList ) + private DirectoryInfo ImportSimpleV2ModPack( ZipArchive extractedModPack, SimpleModPack modList ) { _currentOptionIdx = 0; _currentNumOptions = 1; @@ -125,7 +125,7 @@ public partial class TexToolsImporter } // Extended V2 mod packs contain multiple options that need to be handled separately. - private DirectoryInfo ImportExtendedV2ModPack( ZipFile extractedModPack, string modRaw ) + private DirectoryInfo ImportExtendedV2ModPack( ZipArchive extractedModPack, string modRaw ) { _currentOptionIdx = 0; PluginLog.Log( " -> Importing Extended V2 ModPack" ); diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 26e8d088..f6a4be96 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -55,8 +55,8 @@ - + diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index 57580c05..eced30cb 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -212,7 +212,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod : Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null; _hasSetFolder = true; - _fileManager.OpenFileDialog( "Import Mod Pack", "TexTools Mod Packs{.ttmp,.ttmp2}", ( s, f ) => + _fileManager.OpenFileDialog( "Import Mod Pack", "Mod Packs{.ttmp,.ttmp2,.zip,.7z,.rar},TexTools Mod Packs{.ttmp,.ttmp2},Archives{.zip,.7z,.rar}", ( s, f ) => { if( s ) {