diff --git a/.gitignore b/.gitignore index eb4bc197..0ed7a9ec 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,12 @@ ################################################################################ /.vs/Wabbajack +/BSA.Tools/bin/Debug/netstandard2.0 +/BSA.Tools/obj +/packages +/SevenZipExtractor/bin/Debug +/SevenZipExtractor/obj/Debug +/Wabbajack/bin/Debug +/Wabbajack/obj/Debug +/Wabbajack.Common/bin/Debug +/Wabbajack.Common/obj/Debug diff --git a/BSA.Tools/Archive.cs b/BSA.Tools/Archive.cs new file mode 100644 index 00000000..6460a1a4 --- /dev/null +++ b/BSA.Tools/Archive.cs @@ -0,0 +1,409 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using static BSA.Tools.libbsarch; + +namespace BSA.Tools +{ + // Represents a BSA archive on disk (in READ mode) + public class Archive : IDisposable + { + protected unsafe libbsarch.bsa_archive_t* _archive; + + public UInt32 Version + { + get + { + lock(this) { + unsafe + { + return libbsarch.bsa_version_get(_archive); + } + } + } + } + + public bsa_archive_type_t Type + { + get + { + lock(this) + { + unsafe + { + return libbsarch.bsa_archive_type_get(_archive); + } + } + } + } + + public UInt32 FileCount + { + get + { + lock (this) + { + unsafe + { + return libbsarch.bsa_file_count_get(_archive); + } + } + } + } + + public UInt32 ArchiveFlags + { + get + { + lock (this) + { + unsafe + { + return libbsarch.bsa_archive_flags_get(_archive); + } + } + } + set + { + lock (this) + { + unsafe + { + libbsarch.bsa_archive_flags_set(_archive, value); + } + } + } + } + + public UInt32 FileFlags + { + get + { + lock (this) + { + unsafe + { + return libbsarch.bsa_file_flags_get(_archive); + } + } + } + set + { + lock (this) + { + unsafe + { + libbsarch.bsa_file_flags_set(_archive, value); + } + } + } + } + + public bool Compress + { + get + { + lock (this) + { + unsafe + { + return libbsarch.bsa_compress_get(_archive); + } + } + } + set + { + lock (this) + { + unsafe + { + libbsarch.bsa_compress_set(_archive, value); + } + } + } + } + + public bool ShareData + { + get + { + lock (this) + { + unsafe + { + return libbsarch.bsa_share_data_get(_archive); + } + } + } + set + { + lock (this) + { + unsafe + { + libbsarch.bsa_share_data_set(_archive, value); + } + } + } + } + + + public void Save() + { + lock (this) + { + unsafe + { + check_err(libbsarch.bsa_save(_archive)); + } + } + } + + private IEnumerable _entries = null; + public IEnumerable Entries { + get + { + if (_entries != null) + return _entries; + + return GetAndCacheEntries(); + } + + + } + + private IEnumerable GetAndCacheEntries() + { + var entries = new List(); + unsafe + { + foreach (var filename in GetFileNames()) + { + entries.Add(new ArchiveEntry(this, _archive, filename)); + } + } + _entries = entries; + return entries; + } + + public Archive() + { + unsafe + { + _archive = libbsarch.bsa_create(); + } + } + + public void Create(string filename, bsa_archive_type_t type, EntryList entries) + { + unsafe + { + check_err(libbsarch.bsa_create_archive(_archive, filename, type, entries._list)); + } + } + + public Archive(string filename) + { + unsafe + { + _archive = libbsarch.bsa_create(); + check_err(libbsarch.bsa_load_from_file(_archive, filename)); + } + } + + public void AddFile(string filename, byte[] data) + { + lock(this) + { + unsafe + { + var ptr = Marshal.AllocHGlobal(data.Length); + Marshal.Copy(data, 0, ptr, data.Length); + libbsarch.bsa_add_file_from_memory(_archive, filename, (UInt32)data.Length, (byte*)ptr); + Marshal.FreeHGlobal(ptr); + } + } + } + + public void Dispose() + { + unsafe + { + check_err(libbsarch.bsa_free(_archive)); + } + } + + public static void check_err(libbsarch.bsa_result_message_t bsa_result_message_t) + { + if (bsa_result_message_t.code != 0) + { + unsafe + { + int i = 0; + for (i = 0; i < 1024 * 2; i += 2) + if (bsa_result_message_t.text[i] == 0) break; + + var msg = new String((sbyte*)bsa_result_message_t.text, 0, i, Encoding.Unicode); + throw new Exception(msg); + } + } + } + + public IEnumerable GetFileNames() + { + List filenames = new List(); + lock (this) + { + unsafe + { + check_err(libbsarch.bsa_iterate_files(_archive, (archive, filename, file, folder, context) => + { + lock (filenames) + { + filenames.Add(filename); + } + return false; + }, null)); + } + } + return filenames; + } + } + + public class ArchiveEntry + { + private Archive _archive; + private unsafe libbsarch.bsa_archive_t* _archivep; + private string _filename; + + public string Filename { + get + { + return _filename; + } + } + + public unsafe ArchiveEntry(Archive archive, libbsarch.bsa_archive_t* archivep, string filename) + { + _archive = archive; + _archivep = archivep; + _filename = filename; + } + + public FileData GetFileData() + { + unsafe + { + var result = libbsarch.bsa_extract_file_data_by_filename(_archivep, _filename); + Archive.check_err(result.message); + return new FileData(_archive, _archivep, result.buffer); + } + } + + public void ExtractTo(Stream stream) + { + using (var data = GetFileData()) + { + data.WriteTo(stream); + } + } + + public void ExtractTo(string filename) + { + unsafe + { + libbsarch.bsa_extract_file(_archivep, _filename, filename); + } + } + } + + public class FileData : IDisposable + { + private Archive archive; + private unsafe libbsarch.bsa_archive_t* archivep; + private libbsarch.bsa_result_buffer_t result; + + public unsafe FileData(Archive archive, libbsarch.bsa_archive_t* archivep, libbsarch.bsa_result_buffer_t result) + { + this.archive = archive; + this.archivep = archivep; + this.result = result; + } + + public void WriteTo(Stream stream) + { + var memory = ToByteArray(); + stream.Write(memory, 0, (int)result.size); + } + + public byte[] ToByteArray() + { + unsafe + { + byte[] memory = new byte[result.size]; + Marshal.Copy((IntPtr)result.data, memory, 0, (int)result.size); + return memory; + } + } + + public void Dispose() + { + lock(archive) + unsafe + { + Archive.check_err(libbsarch.bsa_file_data_free(archivep, result)); + } + } + } + + public class EntryList : IDisposable + { + public unsafe bsa_entry_list_t* _list; + + public EntryList() + { + unsafe + { + _list = libbsarch.bsa_entry_list_create(); + } + } + + public UInt32 Count + { + get + { + lock (this) + { + unsafe + { + return libbsarch.bsa_entry_list_count(_list); + } + } + } + } + + public void Add(string entry) + { + lock(this) + { + unsafe + { + libbsarch.bsa_entry_list_add(_list, entry); + } + } + } + + public void Dispose() + { + lock (this) + { + unsafe + { + libbsarch.bsa_entry_list_free(_list); + } + } + } + } +} diff --git a/BSA.Tools/BSA.Tools.csproj b/BSA.Tools/BSA.Tools.csproj new file mode 100644 index 00000000..9c0d635f --- /dev/null +++ b/BSA.Tools/BSA.Tools.csproj @@ -0,0 +1,18 @@ + + + + netstandard2.0 + + + + true + x64 + + + + + PreserveNewest + + + + diff --git a/BSA.Tools/Extensions.cs b/BSA.Tools/Extensions.cs new file mode 100644 index 00000000..7a66f2ce --- /dev/null +++ b/BSA.Tools/Extensions.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace BSA.Tools +{ + public static class Extensions + { + public static string ReadFourCC(this BinaryReader stream) + { + byte[] buf = new byte[4]; + stream.Read(buf, 0, 4); + return new string(buf.Select(b => (char)b).ToArray()); + } + } +} diff --git a/BSA.Tools/libbsarch.cs b/BSA.Tools/libbsarch.cs new file mode 100644 index 00000000..34736610 --- /dev/null +++ b/BSA.Tools/libbsarch.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; + +namespace BSA.Tools +{ + public class libbsarch + { + public struct bsa_archive_t { }; + public struct bsa_file_record_t { }; + public struct bsa_folder_record_t { }; + + public unsafe delegate bool bsa_file_iteration_proc_t(bsa_archive_t archive, [MarshalAs(UnmanagedType.LPWStr)] string file_path, bsa_file_record_t *file_record, bsa_folder_record_t *folder_record, void* context); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe bsa_archive_t* bsa_create(); + + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 1)] + public struct bsa_result_message_t + { + public byte code; // bsa_result_code_t + + public unsafe fixed byte text[1024 * 2]; + + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 1)] + public unsafe struct bsa_result_buffer_t + { + public UInt32 size; + public IntPtr data; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 1)] + public unsafe struct bsa_result_message_buffer_t + { + public bsa_result_buffer_t buffer; + public bsa_result_message_t message; + } + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe bsa_result_message_t bsa_free(bsa_archive_t* t); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe bsa_result_message_t bsa_load_from_file(bsa_archive_t* archive, string file_path); + + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe UInt32 bsa_version_get(bsa_archive_t* archive); + + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe UInt32 bsa_file_count_get(bsa_archive_t* archive); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe bsa_result_message_t bsa_iterate_files(bsa_archive_t *archive, bsa_file_iteration_proc_t file_iteration_proc, void* context); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe bsa_result_message_buffer_t bsa_extract_file_data_by_filename(bsa_archive_t* archive, string file_path); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe bsa_result_message_t bsa_extract_file(bsa_archive_t* archive, string file_path, string save_as); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe bsa_result_message_t bsa_file_data_free(bsa_archive_t* archive, bsa_result_buffer_t file_data_result); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe bsa_archive_type_t bsa_archive_type_get(bsa_archive_t* archive); + + public enum bsa_archive_type_t : Int32 + { + baNone, baTES3, baTES4, baFO3, baSSE, baFO4, baFO4dds + } + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + + public static extern unsafe bsa_result_message_t bsa_add_file_from_memory(bsa_archive_t* archive, string file_path, UInt32 size, byte* data); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe bsa_result_message_t bsa_save(bsa_archive_t* archive); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe bsa_result_message_t bsa_create_archive(bsa_archive_t* archive, string file_path, bsa_archive_type_t archive_type, bsa_entry_list_t* entry_list); + + + // Entry Lists + + public struct bsa_entry_list_t { } + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe bsa_entry_list_t* bsa_entry_list_create(); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe bsa_result_message_t bsa_entry_list_free(bsa_entry_list_t* entry_list); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe UInt32 bsa_entry_list_count(bsa_entry_list_t* entry_list); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe bsa_result_message_t bsa_entry_list_add(bsa_entry_list_t* entry_list, string entry_string); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe UInt32 bsa_entry_list_get(bsa_entry_list_t* entry_list, UInt32 index, UInt32 string_buffer_size, string string_buffer); + + + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe UInt32 bsa_archive_flags_get(bsa_archive_t* archive); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe void bsa_archive_flags_set(bsa_archive_t* archive, UInt32 flags); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe UInt32 bsa_file_flags_get(bsa_archive_t* archive); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe void bsa_file_flags_set(bsa_archive_t* archive, UInt32 flags); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe bool bsa_compress_get(bsa_archive_t* archive); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe void bsa_compress_set(bsa_archive_t* archive, bool flags); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe bool bsa_share_data_get(bsa_archive_t* archive); + + [DllImport("libbsarch.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)] + public static extern unsafe void bsa_share_data_set(bsa_archive_t* archive, bool flags); + + + } + + +} diff --git a/BSA.Tools/libbsarch.dll b/BSA.Tools/libbsarch.dll new file mode 100644 index 00000000..eab88b94 Binary files /dev/null and b/BSA.Tools/libbsarch.dll differ diff --git a/SevenZipExtractor/7zDll/x64/7z.dll b/SevenZipExtractor/7zDll/x64/7z.dll new file mode 100644 index 00000000..a83bdca7 Binary files /dev/null and b/SevenZipExtractor/7zDll/x64/7z.dll differ diff --git a/SevenZipExtractor/7zDll/x86/7z.dll b/SevenZipExtractor/7zDll/x86/7z.dll new file mode 100644 index 00000000..4bcfc182 Binary files /dev/null and b/SevenZipExtractor/7zDll/x86/7z.dll differ diff --git a/SevenZipExtractor/ArchiveFile.cs b/SevenZipExtractor/ArchiveFile.cs new file mode 100644 index 00000000..9111e806 --- /dev/null +++ b/SevenZipExtractor/ArchiveFile.cs @@ -0,0 +1,407 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; + +namespace SevenZipExtractor +{ + public class ArchiveFile : IDisposable + { + private SevenZipHandle sevenZipHandle; + private readonly IInArchive archive; + private readonly InStreamWrapper archiveStream; + private IList entries; + + private string libraryFilePath; + + public ArchiveFile(string archiveFilePath, string libraryFilePath = null) + { + this.libraryFilePath = libraryFilePath; + + this.InitializeAndValidateLibrary(); + + if (!File.Exists(archiveFilePath)) + { + throw new SevenZipException("Archive file not found"); + } + + SevenZipFormat format; + string extension = Path.GetExtension(archiveFilePath); + + if (this.GuessFormatFromExtension(extension, out format)) + { + // great + } + else if (this.GuessFormatFromSignature(archiveFilePath, out format)) + { + // success + } + else + { + throw new SevenZipException(Path.GetFileName(archiveFilePath) + " is not a known archive type"); + } + + this.archive = this.sevenZipHandle.CreateInArchive(Formats.FormatGuidMapping[format]); + this.archiveStream = new InStreamWrapper(File.OpenRead(archiveFilePath)); + } + + public ArchiveFile(Stream archiveStream, SevenZipFormat? format = null, string libraryFilePath = null) + { + this.libraryFilePath = libraryFilePath; + + this.InitializeAndValidateLibrary(); + + if (archiveStream == null) + { + throw new SevenZipException("archiveStream is null"); + } + + if (format == null) + { + SevenZipFormat guessedFormat; + + if (this.GuessFormatFromSignature(archiveStream, out guessedFormat)) + { + format = guessedFormat; + } + else + { + throw new SevenZipException("Unable to guess format automatically"); + } + } + + this.archive = this.sevenZipHandle.CreateInArchive(Formats.FormatGuidMapping[format.Value]); + this.archiveStream = new InStreamWrapper(archiveStream); + } + + public void Extract(string outputFolder, bool overwrite = false) + { + this.Extract(entry => + { + string fileName = Path.Combine(outputFolder, entry.FileName); + + if (entry.IsFolder) + { + return fileName; + } + + if (!File.Exists(fileName) || overwrite) + { + return fileName; + } + + return null; + }); + } + + public void Extract(Func getOutputStream, bool leave_open = false) + { + IList fileStreams = new List(); + + try + { + foreach (Entry entry in Entries) + { + Stream outputStream = getOutputStream(entry); + + if (outputStream == null) // outputStream = null means SKIP + { + fileStreams.Add(null); + continue; + } + + if (entry.IsFolder) + { + fileStreams.Add(null); + continue; + } + + fileStreams.Add(outputStream); + } + + this.archive.Extract(null, 0xFFFFFFFF, 0, new ArchiveStreamsCallback(fileStreams)); + } + finally + { + if (!leave_open) + { + foreach (Stream stream in fileStreams) + { + if (stream != null) + { + stream.Dispose(); + } + } + } + } + } + + public void Extract(Func getOutputPath) + { + IList fileStreams = new List(); + + try + { + foreach (Entry entry in Entries) + { + string outputPath = getOutputPath(entry); + + if (outputPath == null) // getOutputPath = null means SKIP + { + fileStreams.Add(null); + continue; + } + + if (entry.IsFolder) + { + Directory.CreateDirectory(outputPath); + fileStreams.Add(null); + continue; + } + + string directoryName = Path.GetDirectoryName(outputPath); + + if (!string.IsNullOrWhiteSpace(directoryName)) + { + Directory.CreateDirectory(directoryName); + } + + fileStreams.Add(File.Create(outputPath)); + } + + this.archive.Extract(null, 0xFFFFFFFF, 0, new ArchiveStreamsCallback(fileStreams)); + } + finally + { + foreach (Stream stream in fileStreams) + { + if (stream != null) + { + stream.Dispose(); + } + } + } + } + + public IList Entries + { + get + { + if (this.entries != null) + { + return this.entries; + } + + ulong checkPos = 32 * 1024; + int open = this.archive.Open(this.archiveStream, ref checkPos, null); + + if (open != 0) + { + throw new SevenZipException("Unable to open archive"); + } + + uint itemsCount = this.archive.GetNumberOfItems(); + + this.entries = new List(); + + for (uint fileIndex = 0; fileIndex < itemsCount; fileIndex++) + { + string fileName = this.GetProperty(fileIndex, ItemPropId.kpidPath); + bool isFolder = this.GetProperty(fileIndex, ItemPropId.kpidIsFolder); + bool isEncrypted = this.GetProperty(fileIndex, ItemPropId.kpidEncrypted); + ulong size = this.GetProperty(fileIndex, ItemPropId.kpidSize); + ulong packedSize = this.GetProperty(fileIndex, ItemPropId.kpidPackedSize); + DateTime creationTime = this.GetProperty(fileIndex, ItemPropId.kpidCreationTime); + DateTime lastWriteTime = this.GetProperty(fileIndex, ItemPropId.kpidLastWriteTime); + DateTime lastAccessTime = this.GetProperty(fileIndex, ItemPropId.kpidLastAccessTime); + UInt32 crc = this.GetProperty(fileIndex, ItemPropId.kpidCRC); + UInt32 attributes = this.GetProperty(fileIndex, ItemPropId.kpidAttributes); + string comment = this.GetProperty(fileIndex, ItemPropId.kpidComment); + string hostOS = this.GetProperty(fileIndex, ItemPropId.kpidHostOS); + string method = this.GetProperty(fileIndex, ItemPropId.kpidMethod); + + bool isSplitBefore = this.GetProperty(fileIndex, ItemPropId.kpidSplitBefore); + bool isSplitAfter = this.GetProperty(fileIndex, ItemPropId.kpidSplitAfter); + + this.entries.Add(new Entry(this.archive, fileIndex) + { + FileName = fileName, + IsFolder = isFolder, + IsEncrypted = isEncrypted, + Size = size, + PackedSize = packedSize, + CreationTime = creationTime, + LastWriteTime = lastWriteTime, + LastAccessTime = lastAccessTime, + CRC = crc, + Attributes = attributes, + Comment = comment, + HostOS = hostOS, + Method = method, + IsSplitBefore = isSplitBefore, + IsSplitAfter = isSplitAfter + }); + } + + return this.entries; + } + } + + private T GetProperty(uint fileIndex, ItemPropId name) + { + PropVariant propVariant = new PropVariant(); + this.archive.GetProperty(fileIndex, name, ref propVariant); + + T result = propVariant.VarType != VarEnum.VT_EMPTY + ? (T)(dynamic) propVariant.GetObject() + : default(T); + + propVariant.Clear(); + + return result; + } + + private void InitializeAndValidateLibrary() + { + if (string.IsNullOrWhiteSpace(this.libraryFilePath)) + { + string currentArchitecture = IntPtr.Size == 4 ? "x86" : "x64"; // magic check + + if (File.Exists(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "7z-" + currentArchitecture + ".dll"))) + { + this.libraryFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "7z-" + currentArchitecture + ".dll"); + } + else if (File.Exists(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin", "7z-" + currentArchitecture + ".dll"))) + { + this.libraryFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin", "7z-" + currentArchitecture + ".dll"); + } + else if (File.Exists(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, currentArchitecture, "7z.dll"))) + { + this.libraryFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, currentArchitecture, "7z.dll"); + } + else if (File.Exists(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "7-Zip", "7z.dll"))) + { + this.libraryFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "7-Zip", "7z.dll"); + } + } + + if (string.IsNullOrWhiteSpace(this.libraryFilePath)) + { + throw new SevenZipException("libraryFilePath not set"); + } + + if (!File.Exists(this.libraryFilePath)) + { + throw new SevenZipException("7z.dll not found"); + } + + try + { + this.sevenZipHandle = new SevenZipHandle(this.libraryFilePath); + } + catch (Exception e) + { + throw new SevenZipException("Unable to initialize SevenZipHandle", e); + } + } + + private bool GuessFormatFromExtension(string fileExtension, out SevenZipFormat format) + { + if (string.IsNullOrWhiteSpace(fileExtension)) + { + format = SevenZipFormat.Undefined; + return false; + } + + fileExtension = fileExtension.TrimStart('.').Trim().ToLowerInvariant(); + + if (fileExtension.Equals("rar")) + { + // 7z has different GUID for Pre-RAR5 and RAR5, but they have both same extension (.rar) + // If it is [0x52 0x61 0x72 0x21 0x1A 0x07 0x01 0x00] then file is RAR5 otherwise RAR. + // https://www.rarlab.com/technote.htm + + // We are unable to guess right format just by looking at extension and have to check signature + + format = SevenZipFormat.Undefined; + return false; + } + + if (!Formats.ExtensionFormatMapping.ContainsKey(fileExtension)) + { + format = SevenZipFormat.Undefined; + return false; + } + + format = Formats.ExtensionFormatMapping[fileExtension]; + return true; + } + + + private bool GuessFormatFromSignature(string filePath, out SevenZipFormat format) + { + using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + return GuessFormatFromSignature(fileStream, out format); + } + } + + private bool GuessFormatFromSignature(Stream stream, out SevenZipFormat format) + { + int longestSignature = Formats.FileSignatures.Values.OrderByDescending(v => v.Length).First().Length; + + byte[] archiveFileSignature = new byte[longestSignature]; + int bytesRead = stream.Read(archiveFileSignature, 0, longestSignature); + + stream.Position -= bytesRead; // go back o beginning + + if (bytesRead != longestSignature) + { + format = SevenZipFormat.Undefined; + return false; + } + + foreach (KeyValuePair pair in Formats.FileSignatures) + { + if (archiveFileSignature.Take(pair.Value.Length).SequenceEqual(pair.Value)) + { + format = pair.Key; + return true; + } + } + + format = SevenZipFormat.Undefined; + return false; + } + + ~ArchiveFile() + { + this.Dispose(false); + } + + protected void Dispose(bool disposing) + { + if (this.archiveStream != null) + { + this.archiveStream.Dispose(); + } + + if (this.archive != null) + { + Marshal.ReleaseComObject(this.archive); + } + + if (this.sevenZipHandle != null) + { + this.sevenZipHandle.Dispose(); + } + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/SevenZipExtractor/ArchiveFileCallback.cs b/SevenZipExtractor/ArchiveFileCallback.cs new file mode 100644 index 00000000..e86cc636 --- /dev/null +++ b/SevenZipExtractor/ArchiveFileCallback.cs @@ -0,0 +1,57 @@ +using System; +using System.IO; + +namespace SevenZipExtractor +{ + internal class ArchiveFileCallback : IArchiveExtractCallback + { + private readonly string fileName; + private readonly uint fileNumber; + private OutStreamWrapper fileStream; // to be removed + + public ArchiveFileCallback(uint fileNumber, string fileName) + { + this.fileNumber = fileNumber; + this.fileName = fileName; + } + + public void SetTotal(ulong total) + { + } + + public void SetCompleted(ref ulong completeValue) + { + } + + public int GetStream(uint index, out ISequentialOutStream outStream, AskMode askExtractMode) + { + if ((index != this.fileNumber) || (askExtractMode != AskMode.kExtract)) + { + outStream = null; + return 0; + } + + string fileDir = Path.GetDirectoryName(this.fileName); + + if (!string.IsNullOrEmpty(fileDir)) + { + Directory.CreateDirectory(fileDir); + } + + this.fileStream = new OutStreamWrapper(File.Create(this.fileName)); + + outStream = this.fileStream; + + return 0; + } + + public void PrepareOperation(AskMode askExtractMode) + { + } + + public void SetOperationResult(OperationResult resultEOperationResult) + { + this.fileStream.Dispose(); + } + } +} \ No newline at end of file diff --git a/SevenZipExtractor/ArchiveStreamCallback.cs b/SevenZipExtractor/ArchiveStreamCallback.cs new file mode 100644 index 00000000..741e57a0 --- /dev/null +++ b/SevenZipExtractor/ArchiveStreamCallback.cs @@ -0,0 +1,45 @@ +using System.IO; + +namespace SevenZipExtractor +{ + internal class ArchiveStreamCallback : IArchiveExtractCallback + { + private readonly uint fileNumber; + private readonly Stream stream; + + public ArchiveStreamCallback(uint fileNumber, Stream stream) + { + this.fileNumber = fileNumber; + this.stream = stream; + } + + public void SetTotal(ulong total) + { + } + + public void SetCompleted(ref ulong completeValue) + { + } + + public int GetStream(uint index, out ISequentialOutStream outStream, AskMode askExtractMode) + { + if ((index != this.fileNumber) || (askExtractMode != AskMode.kExtract)) + { + outStream = null; + return 0; + } + + outStream = new OutStreamWrapper(this.stream); + + return 0; + } + + public void PrepareOperation(AskMode askExtractMode) + { + } + + public void SetOperationResult(OperationResult resultEOperationResult) + { + } + } +} \ No newline at end of file diff --git a/SevenZipExtractor/ArchiveStreamsCallback.cs b/SevenZipExtractor/ArchiveStreamsCallback.cs new file mode 100644 index 00000000..b8f1fcab --- /dev/null +++ b/SevenZipExtractor/ArchiveStreamsCallback.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.IO; + +namespace SevenZipExtractor +{ + internal class ArchiveStreamsCallback : IArchiveExtractCallback + { + private readonly IList streams; + + public ArchiveStreamsCallback(IList streams) + { + this.streams = streams; + } + + public void SetTotal(ulong total) + { + } + + public void SetCompleted(ref ulong completeValue) + { + } + + public int GetStream(uint index, out ISequentialOutStream outStream, AskMode askExtractMode) + { + if (askExtractMode != AskMode.kExtract) + { + outStream = null; + return 0; + } + + if (this.streams == null) + { + outStream = null; + return 0; + } + + Stream stream = this.streams[(int) index]; + + if (stream == null) + { + outStream = null; + return 0; + } + + outStream = new OutStreamWrapper(stream); + + return 0; + } + + public void PrepareOperation(AskMode askExtractMode) + { + } + + public void SetOperationResult(OperationResult resultEOperationResult) + { + } + } +} \ No newline at end of file diff --git a/SevenZipExtractor/Entry.cs b/SevenZipExtractor/Entry.cs new file mode 100644 index 00000000..4086e901 --- /dev/null +++ b/SevenZipExtractor/Entry.cs @@ -0,0 +1,114 @@ +using System; +using System.IO; + +namespace SevenZipExtractor +{ + public class Entry + { + private readonly IInArchive archive; + private readonly uint index; + + internal Entry(IInArchive archive, uint index) + { + this.archive = archive; + this.index = index; + } + + /// + /// Name of the file with its relative path within the archive + /// + public string FileName { get; internal set; } + /// + /// True if entry is a folder, false if it is a file + /// + public bool IsFolder { get; internal set; } + /// + /// Original entry size + /// + public ulong Size { get; internal set; } + /// + /// Entry size in a archived state + /// + public ulong PackedSize { get; internal set; } + + /// + /// Date and time of the file (entry) creation + /// + public DateTime CreationTime { get; internal set; } + + /// + /// Date and time of the last change of the file (entry) + /// + public DateTime LastWriteTime { get; internal set; } + + /// + /// Date and time of the last access of the file (entry) + /// + public DateTime LastAccessTime { get; internal set; } + + /// + /// CRC hash of the entry + /// + public UInt32 CRC { get; internal set; } + + /// + /// Attributes of the entry + /// + public UInt32 Attributes { get; internal set; } + + /// + /// True if entry is encrypted, otherwise false + /// + public bool IsEncrypted { get; internal set; } + + /// + /// Comment of the entry + /// + public string Comment { get; internal set; } + + /// + /// Compression method of the entry + /// + public string Method { get; internal set; } + + /// + /// Host operating system of the entry + /// + public string HostOS { get; internal set; } + + /// + /// True if there are parts of this file in previous split archive parts + /// + public bool IsSplitBefore { get; set; } + + /// + /// True if there are parts of this file in next split archive parts + /// + public bool IsSplitAfter { get; set; } + + public void Extract(string fileName) + { + if (this.IsFolder) + { + Directory.CreateDirectory(fileName); + return; + } + + string directoryName = Path.GetDirectoryName(fileName); + + if (!string.IsNullOrWhiteSpace(directoryName)) + { + Directory.CreateDirectory(directoryName); + } + + using (FileStream fileStream = File.Create(fileName)) + { + this.Extract(fileStream); + } + } + public void Extract(Stream stream) + { + this.archive.Extract(new[] { this.index }, 1, 0, new ArchiveStreamCallback(this.index, stream)); + } + } +} diff --git a/SevenZipExtractor/Formats.cs b/SevenZipExtractor/Formats.cs new file mode 100644 index 00000000..f5762bc1 --- /dev/null +++ b/SevenZipExtractor/Formats.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; + +namespace SevenZipExtractor +{ + public class Formats + { + internal static readonly Dictionary ExtensionFormatMapping = new Dictionary + { + {"7z", SevenZipFormat.SevenZip}, + {"gz", SevenZipFormat.GZip}, + {"tar", SevenZipFormat.Tar}, + {"rar", SevenZipFormat.Rar}, + {"zip", SevenZipFormat.Zip}, + {"lzma", SevenZipFormat.Lzma}, + {"lzh", SevenZipFormat.Lzh}, + {"arj", SevenZipFormat.Arj}, + {"bz2", SevenZipFormat.BZip2}, + {"cab", SevenZipFormat.Cab}, + {"chm", SevenZipFormat.Chm}, + {"deb", SevenZipFormat.Deb}, + {"iso", SevenZipFormat.Iso}, + {"rpm", SevenZipFormat.Rpm}, + {"wim", SevenZipFormat.Wim}, + {"udf", SevenZipFormat.Udf}, + {"mub", SevenZipFormat.Mub}, + {"xar", SevenZipFormat.Xar}, + {"hfs", SevenZipFormat.Hfs}, + {"dmg", SevenZipFormat.Dmg}, + {"z", SevenZipFormat.Lzw}, + {"xz", SevenZipFormat.XZ}, + {"flv", SevenZipFormat.Flv}, + {"swf", SevenZipFormat.Swf}, + {"exe", SevenZipFormat.PE}, + {"dll", SevenZipFormat.PE}, + {"vhd", SevenZipFormat.Vhd} + }; + + internal static Dictionary FormatGuidMapping = new Dictionary + { + {SevenZipFormat.SevenZip, new Guid("23170f69-40c1-278a-1000-000110070000")}, + {SevenZipFormat.Arj, new Guid("23170f69-40c1-278a-1000-000110040000")}, + {SevenZipFormat.BZip2, new Guid("23170f69-40c1-278a-1000-000110020000")}, + {SevenZipFormat.Cab, new Guid("23170f69-40c1-278a-1000-000110080000")}, + {SevenZipFormat.Chm, new Guid("23170f69-40c1-278a-1000-000110e90000")}, + {SevenZipFormat.Compound, new Guid("23170f69-40c1-278a-1000-000110e50000")}, + {SevenZipFormat.Cpio, new Guid("23170f69-40c1-278a-1000-000110ed0000")}, + {SevenZipFormat.Deb, new Guid("23170f69-40c1-278a-1000-000110ec0000")}, + {SevenZipFormat.GZip, new Guid("23170f69-40c1-278a-1000-000110ef0000")}, + {SevenZipFormat.Iso, new Guid("23170f69-40c1-278a-1000-000110e70000")}, + {SevenZipFormat.Lzh, new Guid("23170f69-40c1-278a-1000-000110060000")}, + {SevenZipFormat.Lzma, new Guid("23170f69-40c1-278a-1000-0001100a0000")}, + {SevenZipFormat.Nsis, new Guid("23170f69-40c1-278a-1000-000110090000")}, + {SevenZipFormat.Rar, new Guid("23170f69-40c1-278a-1000-000110030000")}, + {SevenZipFormat.Rar5, new Guid("23170f69-40c1-278a-1000-000110CC0000")}, + {SevenZipFormat.Rpm, new Guid("23170f69-40c1-278a-1000-000110eb0000")}, + {SevenZipFormat.Split, new Guid("23170f69-40c1-278a-1000-000110ea0000")}, + {SevenZipFormat.Tar, new Guid("23170f69-40c1-278a-1000-000110ee0000")}, + {SevenZipFormat.Wim, new Guid("23170f69-40c1-278a-1000-000110e60000")}, + {SevenZipFormat.Lzw, new Guid("23170f69-40c1-278a-1000-000110050000")}, + {SevenZipFormat.Zip, new Guid("23170f69-40c1-278a-1000-000110010000")}, + {SevenZipFormat.Udf, new Guid("23170f69-40c1-278a-1000-000110E00000")}, + {SevenZipFormat.Xar, new Guid("23170f69-40c1-278a-1000-000110E10000")}, + {SevenZipFormat.Mub, new Guid("23170f69-40c1-278a-1000-000110E20000")}, + {SevenZipFormat.Hfs, new Guid("23170f69-40c1-278a-1000-000110E30000")}, + {SevenZipFormat.Dmg, new Guid("23170f69-40c1-278a-1000-000110E40000")}, + {SevenZipFormat.XZ, new Guid("23170f69-40c1-278a-1000-0001100C0000")}, + {SevenZipFormat.Mslz, new Guid("23170f69-40c1-278a-1000-000110D50000")}, + {SevenZipFormat.PE, new Guid("23170f69-40c1-278a-1000-000110DD0000")}, + {SevenZipFormat.Elf, new Guid("23170f69-40c1-278a-1000-000110DE0000")}, + {SevenZipFormat.Swf, new Guid("23170f69-40c1-278a-1000-000110D70000")}, + {SevenZipFormat.Vhd, new Guid("23170f69-40c1-278a-1000-000110DC0000")}, + {SevenZipFormat.Flv, new Guid("23170f69-40c1-278a-1000-000110D60000")}, + {SevenZipFormat.SquashFS, new Guid("23170f69-40c1-278a-1000-000110D20000")}, + {SevenZipFormat.Lzma86, new Guid("23170f69-40c1-278a-1000-0001100B0000")}, + {SevenZipFormat.Ppmd, new Guid("23170f69-40c1-278a-1000-0001100D0000")}, + {SevenZipFormat.TE, new Guid("23170f69-40c1-278a-1000-000110CF0000")}, + {SevenZipFormat.UEFIc, new Guid("23170f69-40c1-278a-1000-000110D00000")}, + {SevenZipFormat.UEFIs, new Guid("23170f69-40c1-278a-1000-000110D10000")}, + {SevenZipFormat.CramFS, new Guid("23170f69-40c1-278a-1000-000110D30000")}, + {SevenZipFormat.APM, new Guid("23170f69-40c1-278a-1000-000110D40000")}, + {SevenZipFormat.Swfc, new Guid("23170f69-40c1-278a-1000-000110D80000")}, + {SevenZipFormat.Ntfs, new Guid("23170f69-40c1-278a-1000-000110D90000")}, + {SevenZipFormat.Fat, new Guid("23170f69-40c1-278a-1000-000110DA0000")}, + {SevenZipFormat.Mbr, new Guid("23170f69-40c1-278a-1000-000110DB0000")}, + {SevenZipFormat.MachO, new Guid("23170f69-40c1-278a-1000-000110DF0000")} + }; + + internal static Dictionary FileSignatures = new Dictionary + { + {SevenZipFormat.Rar5, new byte[] {0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x01, 0x00}}, + {SevenZipFormat.Rar, new byte[] { 0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00 }}, + {SevenZipFormat.Vhd, new byte[] { 0x63, 0x6F, 0x6E, 0x65, 0x63, 0x74, 0x69, 0x78 }}, + {SevenZipFormat.Deb, new byte[] { 0x21, 0x3C, 0x61, 0x72, 0x63, 0x68, 0x3E }}, + {SevenZipFormat.Dmg, new byte[] { 0x78, 0x01, 0x73, 0x0D, 0x62, 0x62, 0x60 }}, + {SevenZipFormat.SevenZip, new byte[] { 0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C }}, + {SevenZipFormat.Tar, new byte[] { 0x75, 0x73, 0x74, 0x61, 0x72 }}, + {SevenZipFormat.Iso, new byte[] { 0x43, 0x44, 0x30, 0x30, 0x31 }}, + {SevenZipFormat.Cab, new byte[] { 0x4D, 0x53, 0x43, 0x46 }}, + {SevenZipFormat.Rpm, new byte[] { 0xed, 0xab, 0xee, 0xdb }}, + {SevenZipFormat.Xar, new byte[] { 0x78, 0x61, 0x72, 0x21 }}, + {SevenZipFormat.Chm, new byte[] { 0x49, 0x54, 0x53, 0x46 }}, + {SevenZipFormat.BZip2, new byte[] { 0x42, 0x5A, 0x68 }}, + {SevenZipFormat.Flv, new byte[] { 0x46, 0x4C, 0x56 }}, + {SevenZipFormat.Swf, new byte[] { 0x46, 0x57, 0x53 }}, + {SevenZipFormat.GZip, new byte[] { 0x1f, 0x0b }}, + {SevenZipFormat.Zip, new byte[] { 0x50, 0x4b }}, + {SevenZipFormat.Arj, new byte[] { 0x60, 0xEA }}, + {SevenZipFormat.Lzh, new byte[] { 0x2D, 0x6C, 0x68 }} + }; + } +} \ No newline at end of file diff --git a/SevenZipExtractor/IArchiveExtractCallback.cs b/SevenZipExtractor/IArchiveExtractCallback.cs new file mode 100644 index 00000000..94938352 --- /dev/null +++ b/SevenZipExtractor/IArchiveExtractCallback.cs @@ -0,0 +1,23 @@ +using System.Runtime.InteropServices; + +namespace SevenZipExtractor +{ + [ComImport] + [Guid("23170F69-40C1-278A-0000-000600200000")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IArchiveExtractCallback //: IProgress + { + void SetTotal(ulong total); + void SetCompleted([In] ref ulong completeValue); + + [PreserveSig] + int GetStream( + uint index, + [MarshalAs(UnmanagedType.Interface)] out ISequentialOutStream outStream, + AskMode askExtractMode); + // GetStream OUT: S_OK - OK, S_FALSE - skeep this file + + void PrepareOperation(AskMode askExtractMode); + void SetOperationResult(OperationResult resultEOperationResult); + } +} \ No newline at end of file diff --git a/SevenZipExtractor/Kernel32Dll.cs b/SevenZipExtractor/Kernel32Dll.cs new file mode 100644 index 00000000..c69a88a1 --- /dev/null +++ b/SevenZipExtractor/Kernel32Dll.cs @@ -0,0 +1,20 @@ +using System; +using System.Runtime.InteropServices; +using System.Security; + +namespace SevenZipExtractor +{ + internal static class Kernel32Dll + { + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + internal static extern SafeLibraryHandle LoadLibrary([MarshalAs(UnmanagedType.LPTStr)] string lpFileName); + + [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)] + internal static extern IntPtr GetProcAddress(SafeLibraryHandle hModule, [MarshalAs(UnmanagedType.LPStr)] string procName); + + [SuppressUnmanagedCodeSecurity] + [DllImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool FreeLibrary(IntPtr hModule); + } +} \ No newline at end of file diff --git a/SevenZipExtractor/Properties/AssemblyInfo.cs b/SevenZipExtractor/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..ab29e195 --- /dev/null +++ b/SevenZipExtractor/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SevenZipWrapper")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SevenZipWrapper")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("8aa97f58-5044-4bba-b8d9-a74b6947a660")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SevenZipExtractor/SafeLibraryHandle.cs b/SevenZipExtractor/SafeLibraryHandle.cs new file mode 100644 index 00000000..4369cfa5 --- /dev/null +++ b/SevenZipExtractor/SafeLibraryHandle.cs @@ -0,0 +1,21 @@ +using System; +using System.Runtime.ConstrainedExecution; +using Microsoft.Win32.SafeHandles; + +namespace SevenZipExtractor +{ + internal sealed class SafeLibraryHandle : SafeHandleZeroOrMinusOneIsInvalid + { + public SafeLibraryHandle() : base(true) + { + } + + /// Release library handle + /// true if the handle was released + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] + protected override bool ReleaseHandle() + { + return Kernel32Dll.FreeLibrary(this.handle); + } + } +} \ No newline at end of file diff --git a/SevenZipExtractor/SevenZipException.cs b/SevenZipExtractor/SevenZipException.cs new file mode 100644 index 00000000..b4f66760 --- /dev/null +++ b/SevenZipExtractor/SevenZipException.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.Serialization; + +namespace SevenZipExtractor +{ + public class SevenZipException : Exception + { + public SevenZipException() + { + } + + public SevenZipException(string message) : base(message) + { + } + + public SevenZipException(string message, Exception innerException) : base(message, innerException) + { + } + + protected SevenZipException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} \ No newline at end of file diff --git a/SevenZipExtractor/SevenZipExtractor.csproj b/SevenZipExtractor/SevenZipExtractor.csproj new file mode 100644 index 00000000..1a2059c4 --- /dev/null +++ b/SevenZipExtractor/SevenZipExtractor.csproj @@ -0,0 +1,82 @@ + + + + + Debug + AnyCPU + {8AA97F58-5044-4BBA-B8D9-A74B6947A660} + Library + Properties + SevenZipExtractor + SevenZipExtractor + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + + + + + \ No newline at end of file diff --git a/SevenZipExtractor/SevenZipExtractor.nuspec b/SevenZipExtractor/SevenZipExtractor.nuspec new file mode 100644 index 00000000..3e44d1ab --- /dev/null +++ b/SevenZipExtractor/SevenZipExtractor.nuspec @@ -0,0 +1,21 @@ + + + + SevenZipExtractor + 1.0.12 + SevenZipExtractor + Eugene Sichkar, Alexander Selishchev, @matortheeternal, Hajin Jang, Artem Tarasov, Jose Pineiro, Raphael Stoeckli + Alexander Selishchev + https://github.com/adoconnection/SevenZipExtractor + false + C# wrapper for 7z.dll (included) + + Copyright 2018 + 7Zip APM Arj BZip2 Cab Chm Compound Cpio CramFS Deb Dll Dmg Exe Fat Flv GZip Hfs Iso Lzh Lzma Lzma86 Mach-O Mbr Mub Nsis Ntfs Ppmd Rar Rar5 Rpm Split SquashFS Swf Swfc Tar TE Udf UEFIc UEFIs Vhd Wim Xar XZ Z Zip + + + + + + + \ No newline at end of file diff --git a/SevenZipExtractor/SevenZipExtractor.targets b/SevenZipExtractor/SevenZipExtractor.targets new file mode 100644 index 00000000..d2a865f1 --- /dev/null +++ b/SevenZipExtractor/SevenZipExtractor.targets @@ -0,0 +1,107 @@ + + + + + + + true + + + + + + + + + + + + + %(RecursiveDir)%(FileName)%(Extension) + Always + + + + + + + + + + + + + + + + + + + + + + $(PostBuildEventDependsOn); + CopyDllFiles; + + + $(BuildDependsOn); + CopyDllFiles; + + + $(CleanDependsOn); + CleanDllFiles; + + + diff --git a/SevenZipExtractor/SevenZipFormat.cs b/SevenZipExtractor/SevenZipFormat.cs new file mode 100644 index 00000000..c95eae54 --- /dev/null +++ b/SevenZipExtractor/SevenZipFormat.cs @@ -0,0 +1,285 @@ +namespace SevenZipExtractor +{ + /// + /// + /// + public enum SevenZipFormat + { + // Default invalid format value + Undefined = 0, + + /// + /// Open 7-zip archive format. + /// + /// Wikipedia information + SevenZip, + + /// + /// Proprietary Arj archive format. + /// + /// Wikipedia information + Arj, + + /// + /// Open Bzip2 archive format. + /// + /// Wikipedia information + BZip2, + + /// + /// Microsoft cabinet archive format. + /// + /// Wikipedia information + Cab, + + /// + /// Microsoft Compiled HTML Help file format. + /// + /// Wikipedia information + Chm, + + /// + /// Microsoft Compound file format. + /// + /// Wikipedia information + Compound, + + /// + /// Open Cpio archive format. + /// + /// Wikipedia information + Cpio, + + /// + /// Open Debian software package format. + /// + /// Wikipedia information + Deb, + + /// + /// Open Gzip archive format. + /// + /// Wikipedia information + GZip, + + /// + /// Open ISO disk image format. + /// + /// Wikipedia information + Iso, + + /// + /// Open Lzh archive format. + /// + /// Wikipedia information + Lzh, + + /// + /// Open core 7-zip Lzma raw archive format. + /// + /// Wikipedia information + Lzma, + + /// + /// Nullsoft installation package format. + /// + /// Wikipedia information + Nsis, + + /// + /// RarLab Rar archive format. + /// + /// Wikipedia information + Rar, + + /// + /// RarLab Rar archive format, version 5. + /// + /// Wikipedia information + Rar5, + + /// + /// Open Rpm software package format. + /// + /// Wikipedia information + Rpm, + + /// + /// Open split file format. + /// + /// Wikipedia information + Split, + + /// + /// Open Tar archive format. + /// + /// Wikipedia information + Tar, + + /// + /// Microsoft Windows Imaging disk image format. + /// + /// Wikipedia information + Wim, + + /// + /// Open LZW archive format; implemented in "compress" program; also known as "Z" archive format. + /// + /// Wikipedia information + Lzw, + + /// + /// Open Zip archive format. + /// + /// Wikipedia information + Zip, + + /// + /// Open Udf disk image format. + /// + Udf, + + /// + /// Xar open source archive format. + /// + /// Wikipedia information + Xar, + + /// + /// Mub + /// + Mub, + + /// + /// Macintosh Disk Image on CD. + /// + /// Wikipedia information + Hfs, + + /// + /// Apple Mac OS X Disk Copy Disk Image format. + /// + Dmg, + + /// + /// Open Xz archive format. + /// + /// Wikipedia information + XZ, + + /// + /// MSLZ archive format. + /// + Mslz, + + /// + /// Flash video format. + /// + /// Wikipedia information + Flv, + + /// + /// Shockwave Flash format. + /// + /// Wikipedia information + Swf, + + /// + /// Windows PE executable format. + /// + /// Wikipedia information + PE, + + /// + /// Linux executable Elf format. + /// + /// Wikipedia information + Elf, + + /// + /// Windows Installer Database. + /// + /// Wikipedia information + Msi, + + /// + /// Microsoft virtual hard disk file format. + /// + /// Wikipedia information + Vhd, + + /// + /// SquashFS file system format. + /// + /// Wikipedia information + SquashFS, + + /// + /// Lzma86 file format. + /// + Lzma86, + + /// + /// Prediction by Partial Matching by Dmitry algorithm. + /// + /// Wikipedia information + Ppmd, + + /// + /// TE format. + /// + TE, + + /// + /// UEFIc format. + /// + /// Wikipedia information + UEFIc, + + /// + /// UEFIs format. + /// + /// Wikipedia information + UEFIs, + + /// + /// Compressed ROM file system format. + /// + /// Wikipedia information + CramFS, + + /// + /// APM format. + /// + APM, + + /// + /// Swfc format. + /// + Swfc, + + /// + /// NTFS file system format. + /// + /// Wikipedia information + Ntfs, + + /// + /// FAT file system format. + /// + /// Wikipedia information + Fat, + + /// + /// MBR format. + /// + /// Wikipedia information + Mbr, + + /// + /// Mach-O file format. + /// + /// Wikipedia information + MachO + } +} \ No newline at end of file diff --git a/SevenZipExtractor/SevenZipHandle.cs b/SevenZipExtractor/SevenZipHandle.cs new file mode 100644 index 00000000..8fa45fbc --- /dev/null +++ b/SevenZipExtractor/SevenZipHandle.cs @@ -0,0 +1,68 @@ +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; + +namespace SevenZipExtractor +{ + internal class SevenZipHandle : IDisposable + { + private SafeLibraryHandle sevenZipSafeHandle; + + public SevenZipHandle(string sevenZipLibPath) + { + this.sevenZipSafeHandle = Kernel32Dll.LoadLibrary(sevenZipLibPath); + + if (this.sevenZipSafeHandle.IsInvalid) + { + throw new Win32Exception(); + } + + IntPtr functionPtr = Kernel32Dll.GetProcAddress(this.sevenZipSafeHandle, "GetHandlerProperty"); + + // Not valid dll + if (functionPtr == IntPtr.Zero) + { + this.sevenZipSafeHandle.Close(); + throw new ArgumentException(); + } + } + + ~SevenZipHandle() + { + this.Dispose(false); + } + + protected void Dispose(bool disposing) + { + if ((this.sevenZipSafeHandle != null) && !this.sevenZipSafeHandle.IsClosed) + { + this.sevenZipSafeHandle.Close(); + } + + this.sevenZipSafeHandle = null; + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + public IInArchive CreateInArchive(Guid classId) + { + if (this.sevenZipSafeHandle == null) + { + throw new ObjectDisposedException("SevenZipHandle"); + } + + IntPtr procAddress = Kernel32Dll.GetProcAddress(this.sevenZipSafeHandle, "CreateObject"); + CreateObjectDelegate createObject = (CreateObjectDelegate) Marshal.GetDelegateForFunctionPointer(procAddress, typeof (CreateObjectDelegate)); + + object result; + Guid interfaceId = typeof (IInArchive).GUID; + createObject(ref classId, ref interfaceId, out result); + + return result as IInArchive; + } + } +} \ No newline at end of file diff --git a/SevenZipExtractor/SevenZipInterface.cs b/SevenZipExtractor/SevenZipInterface.cs new file mode 100644 index 00000000..8e6ad061 --- /dev/null +++ b/SevenZipExtractor/SevenZipInterface.cs @@ -0,0 +1,529 @@ +// Version 1.5 + +using System; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Security.Permissions; +using System.Threading; + +namespace SevenZipExtractor +{ + [StructLayout(LayoutKind.Sequential)] + internal struct PropArray + { + uint length; + IntPtr pointerValues; + } + + [StructLayout(LayoutKind.Explicit)] + internal struct PropVariant + { + [DllImport("ole32.dll")] + private static extern int PropVariantClear(ref PropVariant pvar); + + [FieldOffset(0)] public ushort vt; + [FieldOffset(8)] public IntPtr pointerValue; + [FieldOffset(8)] public byte byteValue; + [FieldOffset(8)] public long longValue; + [FieldOffset(8)] public System.Runtime.InteropServices.ComTypes.FILETIME filetime; + [FieldOffset(8)] public PropArray propArray; + + public VarEnum VarType + { + get + { + return (VarEnum) this.vt; + } + } + + public void Clear() + { + switch (this.VarType) + { + case VarEnum.VT_EMPTY: + break; + + case VarEnum.VT_NULL: + case VarEnum.VT_I2: + case VarEnum.VT_I4: + case VarEnum.VT_R4: + case VarEnum.VT_R8: + case VarEnum.VT_CY: + case VarEnum.VT_DATE: + case VarEnum.VT_ERROR: + case VarEnum.VT_BOOL: + //case VarEnum.VT_DECIMAL: + case VarEnum.VT_I1: + case VarEnum.VT_UI1: + case VarEnum.VT_UI2: + case VarEnum.VT_UI4: + case VarEnum.VT_I8: + case VarEnum.VT_UI8: + case VarEnum.VT_INT: + case VarEnum.VT_UINT: + case VarEnum.VT_HRESULT: + case VarEnum.VT_FILETIME: + this.vt = 0; + break; + + default: + PropVariantClear(ref this); + break; + } + } + + public object GetObject() + { + switch (this.VarType) + { + case VarEnum.VT_EMPTY: + return null; + + case VarEnum.VT_FILETIME: + return DateTime.FromFileTime(this.longValue); + + default: + GCHandle PropHandle = GCHandle.Alloc(this, GCHandleType.Pinned); + + try + { + return Marshal.GetObjectForNativeVariant(PropHandle.AddrOfPinnedObject()); + } + finally + { + PropHandle.Free(); + } + } + } + } + + [ComImport] + [Guid("23170F69-40C1-278A-0000-000000050000")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IProgress + { + void SetTotal(ulong total); + void SetCompleted([In] ref ulong completeValue); + } + + [ComImport] + [Guid("23170F69-40C1-278A-0000-000600100000")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IArchiveOpenCallback + { + // ref ulong replaced with IntPtr because handlers ofter pass null value + // read actual value with Marshal.ReadInt64 + void SetTotal( + IntPtr files, // [In] ref ulong files, can use 'ulong* files' but it is unsafe + IntPtr bytes); // [In] ref ulong bytes + + void SetCompleted( + IntPtr files, // [In] ref ulong files + IntPtr bytes); // [In] ref ulong bytes + } + + [ComImport] + [Guid("23170F69-40C1-278A-0000-000500100000")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ICryptoGetTextPassword + { + [PreserveSig] + int CryptoGetTextPassword( + [MarshalAs(UnmanagedType.BStr)] out string password); + + //[return : MarshalAs(UnmanagedType.BStr)] + //string CryptoGetTextPassword(); + } + + internal enum AskMode : int + { + kExtract = 0, + kTest, + kSkip + } + + internal enum OperationResult : int + { + kOK = 0, + kUnSupportedMethod, + kDataError, + kCRCError + } + + [ComImport] + [Guid("23170F69-40C1-278A-0000-000600300000")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IArchiveOpenVolumeCallback + { + void GetProperty( + ItemPropId propID, // PROPID + IntPtr value); // PROPVARIANT + + [PreserveSig] + int GetStream( + [MarshalAs(UnmanagedType.LPWStr)] string name, + [MarshalAs(UnmanagedType.Interface)] out IInStream inStream); + } + + [ComImport] + [Guid("23170F69-40C1-278A-0000-000600400000")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IInArchiveGetStream + { + [return: MarshalAs(UnmanagedType.Interface)] + ISequentialInStream GetStream(uint index); + } + + [ComImport] + [Guid("23170F69-40C1-278A-0000-000300010000")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISequentialInStream + { + //[PreserveSig] + //int Read( + // [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] data, + // uint size, + // IntPtr processedSize); // ref uint processedSize + + uint Read( + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] data, + uint size); + + /* + Out: if size != 0, return_value = S_OK and (*processedSize == 0), + then there are no more bytes in stream. + if (size > 0) && there are bytes in stream, + this function must read at least 1 byte. + This function is allowed to read less than number of remaining bytes in stream. + You must call Read function in loop, if you need exact amount of data + */ + } + + [ComImport] + [Guid("23170F69-40C1-278A-0000-000300020000")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface ISequentialOutStream + { + [PreserveSig] + int Write( + [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] data, + uint size, + IntPtr processedSize); // ref uint processedSize + /* + if (size > 0) this function must write at least 1 byte. + This function is allowed to write less than "size". + You must call Write function in loop, if you need to write exact amount of data + */ + } + + [ComImport] + [Guid("23170F69-40C1-278A-0000-000300030000")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IInStream //: ISequentialInStream + { + //[PreserveSig] + //int Read( + // [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] data, + // uint size, + // IntPtr processedSize); // ref uint processedSize + + uint Read( + [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] data, + uint size); + + //[PreserveSig] + void Seek( + long offset, + uint seekOrigin, + IntPtr newPosition); // ref long newPosition + } + + [ComImport] + [Guid("23170F69-40C1-278A-0000-000300040000")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IOutStream //: ISequentialOutStream + { + [PreserveSig] + int Write( + [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] data, + uint size, + IntPtr processedSize); // ref uint processedSize + + //[PreserveSig] + void Seek( + long offset, + uint seekOrigin, + IntPtr newPosition); // ref long newPosition + + [PreserveSig] + int SetSize(long newSize); + } + + internal enum ItemPropId : uint + { + kpidNoProperty = 0, + + kpidHandlerItemIndex = 2, + kpidPath, + kpidName, + kpidExtension, + kpidIsFolder, + kpidSize, + kpidPackedSize, + kpidAttributes, + kpidCreationTime, + kpidLastAccessTime, + kpidLastWriteTime, + kpidSolid, + kpidCommented, + kpidEncrypted, + kpidSplitBefore, + kpidSplitAfter, + kpidDictionarySize, + kpidCRC, + kpidType, + kpidIsAnti, + kpidMethod, + kpidHostOS, + kpidFileSystem, + kpidUser, + kpidGroup, + kpidBlock, + kpidComment, + kpidPosition, + kpidPrefix, + + kpidTotalSize = 0x1100, + kpidFreeSpace, + kpidClusterSize, + kpidVolumeName, + + kpidLocalName = 0x1200, + kpidProvider, + + kpidUserDefined = 0x10000 + } + + + [ComImport] + [Guid("23170F69-40C1-278A-0000-000600600000")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + //[AutomationProxy(true)] + internal interface IInArchive + { + [PreserveSig] + int Open( + IInStream stream, + /*[MarshalAs(UnmanagedType.U8)]*/ [In] ref ulong maxCheckStartPosition, + [MarshalAs(UnmanagedType.Interface)] IArchiveOpenCallback openArchiveCallback); + + void Close(); + //void GetNumberOfItems([In] ref uint numItem); + uint GetNumberOfItems(); + + void GetProperty( + uint index, + ItemPropId propID, // PROPID + ref PropVariant value); // PROPVARIANT + + [PreserveSig] + int Extract( + [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] uint[] indices, //[In] ref uint indices, + uint numItems, + int testMode, + [MarshalAs(UnmanagedType.Interface)] IArchiveExtractCallback extractCallback); + + // indices must be sorted + // numItems = 0xFFFFFFFF means all files + // testMode != 0 means "test files operation" + + void GetArchiveProperty( + uint propID, // PROPID + ref PropVariant value); // PROPVARIANT + + //void GetNumberOfProperties([In] ref uint numProperties); + uint GetNumberOfProperties(); + + void GetPropertyInfo( + uint index, + [MarshalAs(UnmanagedType.BStr)] out string name, + out ItemPropId propID, // PROPID + out ushort varType); //VARTYPE + + //void GetNumberOfArchiveProperties([In] ref uint numProperties); + uint GetNumberOfArchiveProperties(); + + void GetArchivePropertyInfo( + uint index, + [MarshalAs(UnmanagedType.BStr)] string name, + ref uint propID, // PROPID + ref ushort varType); //VARTYPE + } + + internal enum ArchivePropId : uint + { + kName = 0, + kClassID, + kExtension, + kAddExtension, + kUpdate, + kKeepName, + kStartSignature, + kFinishSignature, + kAssociate + } + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate int CreateObjectDelegate( + [In] ref Guid classID, + [In] ref Guid interfaceID, + //out IntPtr outObject); + [MarshalAs(UnmanagedType.Interface)] out object outObject); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate int GetHandlerPropertyDelegate( + ArchivePropId propID, + ref PropVariant value); // PROPVARIANT + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate int GetNumberOfFormatsDelegate(out uint numFormats); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + internal delegate int GetHandlerProperty2Delegate( + uint formatIndex, + ArchivePropId propID, + ref PropVariant value); // PROPVARIANT + + internal class StreamWrapper : IDisposable + { + protected Stream BaseStream; + + protected StreamWrapper(Stream baseStream) + { + this.BaseStream = baseStream; + } + + public void Dispose() + { + this.BaseStream.Close(); + } + + public virtual void Seek(long offset, uint seekOrigin, IntPtr newPosition) + { + long Position = (uint) this.BaseStream.Seek(offset, (SeekOrigin) seekOrigin); + if (newPosition != IntPtr.Zero) + { + Marshal.WriteInt64(newPosition, Position); + } + } + } + + internal class InStreamWrapper : StreamWrapper, ISequentialInStream, IInStream + { + public InStreamWrapper(Stream baseStream) : base(baseStream) + { + } + + public uint Read(byte[] data, uint size) + { + return (uint) this.BaseStream.Read(data, 0, (int) size); + } + } + + // Can close base stream after period of inactivity and reopen it when needed. + // Useful for long opened archives (prevent locking archive file on disk). + internal class InStreamTimedWrapper : StreamWrapper, ISequentialInStream, IInStream + { + private string BaseStreamFileName; + private long BaseStreamLastPosition; + private Timer CloseTimer; + + private const int KeepAliveInterval = 10 * 1000; // 10 sec + + public InStreamTimedWrapper(Stream baseStream) + : base(baseStream) + { + if ((this.BaseStream is FileStream) && !this.BaseStream.CanWrite && this.BaseStream.CanSeek) + { + this.BaseStreamFileName = ((FileStream) this.BaseStream).Name; + this.CloseTimer = new Timer(new TimerCallback(this.CloseStream), null, KeepAliveInterval, Timeout.Infinite); + } + } + + private void CloseStream(object state) + { + if (this.CloseTimer != null) + { + this.CloseTimer.Dispose(); + this.CloseTimer = null; + } + + if (this.BaseStream != null) + { + if (this.BaseStream.CanSeek) + { + this.BaseStreamLastPosition = this.BaseStream.Position; + } + this.BaseStream.Close(); + this.BaseStream = null; + } + } + + protected void ReopenStream() + { + if (this.BaseStream == null) + { + if (this.BaseStreamFileName != null) + { + this.BaseStream = new FileStream(this.BaseStreamFileName, FileMode.Open, FileAccess.Read, FileShare.Read); + this.BaseStream.Position = this.BaseStreamLastPosition; + this.CloseTimer = new Timer(new TimerCallback(this.CloseStream), null, KeepAliveInterval, Timeout.Infinite); + } + else + { + throw new ObjectDisposedException("StreamWrapper"); + } + } + else if (this.CloseTimer != null) + { + this.CloseTimer.Change(KeepAliveInterval, Timeout.Infinite); + } + } + + public uint Read(byte[] data, uint size) + { + this.ReopenStream(); + return (uint) this.BaseStream.Read(data, 0, (int) size); + } + + public override void Seek(long offset, uint seekOrigin, IntPtr newPosition) + { + this.ReopenStream(); + base.Seek(offset, seekOrigin, newPosition); + } + } + + internal class OutStreamWrapper : StreamWrapper, ISequentialOutStream, IOutStream + { + public OutStreamWrapper(Stream baseStream) : base(baseStream) + { + } + + public int SetSize(long newSize) + { + this.BaseStream.SetLength(newSize); + return 0; + } + + public int Write(byte[] data, uint size, IntPtr processedSize) + { + this.BaseStream.Write(data, 0, (int) size); + if (processedSize != IntPtr.Zero) + { + Marshal.WriteInt32(processedSize, (int) size); + } + return 0; + } + } +} \ No newline at end of file diff --git a/Wabbajack.Common/Consts.cs b/Wabbajack.Common/Consts.cs new file mode 100644 index 00000000..4e839f10 --- /dev/null +++ b/Wabbajack.Common/Consts.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Wabbajack.Common +{ + public static class Consts + { + public static string GameFolderFilesDir = "Game Folder Files"; + } +} diff --git a/Wabbajack.Common/Data.cs b/Wabbajack.Common/Data.cs new file mode 100644 index 00000000..0c463b37 --- /dev/null +++ b/Wabbajack.Common/Data.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Wabbajack.Common +{ + public class RawSourceFile + { + public string AbsolutePath; + public string Path; + + private string _hash; + public string Hash + { + get + { + if (_hash != null) return _hash; + _hash = AbsolutePath.FileSHA256(); + return _hash; + } + } + + public T EvolveTo() where T : Directive, new() + { + var v = new T(); + v.To = Path; + return v; + } + } + + public class ModList + { + /// + /// Name of the ModList + /// + public string Name; + /// + /// Author of the Mod List + /// + public string Author; + /// + /// Version of this Mod List + /// + public string Version; + + /// + /// Install directives + /// + public List Directives; + + /// + /// Archives required by this modlist + /// + public List Archives; + } + + public class Directive + { + /// + /// location the file will be copied to, relative to the install path. + /// + public string To; + } + + public class IgnoredDirectly : Directive + { + public string Reason; + } + + public class NoMatch : IgnoredDirectly + { + + } + + public class InlineFile : Directive + { + /// + /// Data that will be written as-is to the destination location; + /// + public string SourceData; + } + + public class FromArchive : Directive + { + /// + /// MurMur3 hash of the archive this file comes from + /// + public string ArchiveHash; + /// + /// The relative path of the file in the archive + /// + public string From; + } + + public class PatchedArchive : FromArchive + { + /// + /// The file to apply to the source file to patch it + /// + public string Patch; + } + + public class Archive + { + /// + /// MurMur3 Hash of the archive + /// + public string Hash; + /// + /// Human friendly name of this archive + /// + public string Name; + } + + /// + /// URL that can be downloaded directly without any additional options + /// + public class DirectURLArchive : Archive + { + public string URL; + } + + /// + /// An archive that requires additional HTTP headers. + /// + public class DirectURLArchiveEx : DirectURLArchive + { + public Dictionary Headers; + } + + /// + /// Archive that comes from MEGA + /// + public class MEGAArchive : DirectURLArchive + { + } + + /// + /// Archive that comes from MODDB + /// + public class MODDBArchive : DirectURLArchive + { + } + + /// + /// The indexed contents of an archive + /// + public class IndexedArchiveCache + { + public string Hash; + public List Entries; + } + + public class IndexedArchive : IndexedArchiveCache + { + public dynamic IniData; + public string Name; + } + + /// + /// A archive entry + /// + public class IndexedEntry + { + /// + /// Path in the archive to this file + /// + public string Path; + /// + /// MurMur3 hash of this file + /// + public string Hash; + /// + /// Size of the file (uncompressed) + /// + public long Size; + } + + /// + /// Data found inside a BSA file in an archive + /// + public class BSAIndexedEntry : IndexedEntry + { + /// + /// MurMur3 hash of the BSA this file comes from + /// + public string BSAHash; + } +} diff --git a/Wabbajack.Common/DynamicIniData.cs b/Wabbajack.Common/DynamicIniData.cs new file mode 100644 index 00000000..13c63bf6 --- /dev/null +++ b/Wabbajack.Common/DynamicIniData.cs @@ -0,0 +1,53 @@ +using IniParser; +using IniParser.Model; +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Text; + +namespace Wabbajack.Common +{ + public class DynamicIniData : DynamicObject + { + private IniData value; + + public DynamicIniData(IniData value) // + { + this.value = value; + } + + public static dynamic FromIni(IniData data) + { + return new DynamicIniData(data); + } + + public static dynamic FromFile(string filename) + { + var fi = new FileIniDataParser(); + return new DynamicIniData(fi.ReadFile(filename)); + } + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + result = new SectionData(value[binder.Name]); + return true; + } + } + + class SectionData : DynamicObject + { + private KeyDataCollection _coll; + + public SectionData(KeyDataCollection coll) + { + this._coll = coll; + } + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + result = _coll[binder.Name]; + return true; + } + } + +} diff --git a/Wabbajack.Common/Properties/AssemblyInfo.cs b/Wabbajack.Common/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..fbfcdefc --- /dev/null +++ b/Wabbajack.Common/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Wabbajack.Common")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Wabbajack.Common")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("b3f3fb6e-b9eb-4f49-9875-d78578bc7ae5")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Wabbajack.Common/Utils.cs b/Wabbajack.Common/Utils.cs new file mode 100644 index 00000000..0c450a15 --- /dev/null +++ b/Wabbajack.Common/Utils.cs @@ -0,0 +1,88 @@ +using IniParser; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace Wabbajack.Common +{ + public static class Utils + { + + + + /// + /// MurMur3 hashes the file pointed to by this string + /// + /// + /// + public static string FileSHA256(this string file) + { + var sha = new SHA256Managed(); + using (var o = new CryptoStream(Stream.Null, sha, CryptoStreamMode.Write)) + { + using (var i = File.OpenRead(file)) + i.CopyTo(o); + } + return sha.Hash.ToBase64(); + + } + + /// + /// Returns a Base64 encoding of these bytes + /// + /// + /// + public static string ToBase64(this byte[] data) + { + return Convert.ToBase64String(data); + } + + /// + /// Executes the action for every item in coll + /// + /// + /// + /// + public static void Do(this IEnumerable coll, Action f) + { + foreach (var i in coll) f(i); + } + + /// + /// Loads INI data from the given filename and returns a dynamic type that + /// can use . operators to navigate the INI. + /// + /// + /// + public static dynamic LoadIniFile(this string file) + { + return new DynamicIniData(new FileIniDataParser().ReadFile(file)); + } + + public static void ToJSON(this T obj, string filename) + { + File.WriteAllText(filename, JsonConvert.SerializeObject(obj, Formatting.Indented, new JsonSerializerSettings() {TypeNameHandling = TypeNameHandling.Auto})); + } + + public static T FromJSON(this string filename) + { + return JsonConvert.DeserializeObject(File.ReadAllText(filename)); + } + + public static bool FileExists(this string filename) + { + return File.Exists(filename); + } + + public static string RelativeTo(this string file, string folder) + { + return file.Substring(folder.Length + 1); + } + + } +} diff --git a/Wabbajack.Common/Wabbajack.Common.csproj b/Wabbajack.Common/Wabbajack.Common.csproj new file mode 100644 index 00000000..e1410ca9 --- /dev/null +++ b/Wabbajack.Common/Wabbajack.Common.csproj @@ -0,0 +1,71 @@ + + + + + Debug + AnyCPU + {B3F3FB6E-B9EB-4F49-9875-D78578BC7AE5} + Library + Properties + Wabbajack.Common + Wabbajack.Common + v4.7.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + x64 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\ini-parser.2.5.2\lib\net20\INIFileParser.dll + + + ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll + + + + + + + + + + + + + + + + + + + + + + + {b6389807-ea59-4633-b42b-a85f52fec135} + BSA.Tools + + + {8aa97f58-5044-4bba-b8d9-a74b6947a660} + SevenZipExtractor + + + + \ No newline at end of file diff --git a/Wabbajack.Common/packages.config b/Wabbajack.Common/packages.config new file mode 100644 index 00000000..d4d1fb83 --- /dev/null +++ b/Wabbajack.Common/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Wabbajack.sln b/Wabbajack.sln new file mode 100644 index 00000000..ddc035ba --- /dev/null +++ b/Wabbajack.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29102.190 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack", "Wabbajack\Wabbajack.csproj", "{1649CAB3-6F8E-4F2E-844C-931BD4B61233}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.Common", "Wabbajack.Common\Wabbajack.Common.csproj", "{B3F3FB6E-B9EB-4F49-9875-D78578BC7AE5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BSA.Tools", "BSA.Tools\BSA.Tools.csproj", "{B6389807-EA59-4633-B42B-A85F52FEC135}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SevenZipExtractor", "SevenZipExtractor\SevenZipExtractor.csproj", "{8AA97F58-5044-4BBA-B8D9-A74B6947A660}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1649CAB3-6F8E-4F2E-844C-931BD4B61233}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1649CAB3-6F8E-4F2E-844C-931BD4B61233}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1649CAB3-6F8E-4F2E-844C-931BD4B61233}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1649CAB3-6F8E-4F2E-844C-931BD4B61233}.Release|Any CPU.Build.0 = Release|Any CPU + {B3F3FB6E-B9EB-4F49-9875-D78578BC7AE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B3F3FB6E-B9EB-4F49-9875-D78578BC7AE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B3F3FB6E-B9EB-4F49-9875-D78578BC7AE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B3F3FB6E-B9EB-4F49-9875-D78578BC7AE5}.Release|Any CPU.Build.0 = Release|Any CPU + {B6389807-EA59-4633-B42B-A85F52FEC135}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6389807-EA59-4633-B42B-A85F52FEC135}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6389807-EA59-4633-B42B-A85F52FEC135}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6389807-EA59-4633-B42B-A85F52FEC135}.Release|Any CPU.Build.0 = Release|Any CPU + {8AA97F58-5044-4BBA-B8D9-A74B6947A660}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8AA97F58-5044-4BBA-B8D9-A74B6947A660}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8AA97F58-5044-4BBA-B8D9-A74B6947A660}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8AA97F58-5044-4BBA-B8D9-A74B6947A660}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {5CF408D5-8BF2-40C2-907C-694DBACFF01B} + EndGlobalSection +EndGlobal diff --git a/Wabbajack/App.config b/Wabbajack/App.config new file mode 100644 index 00000000..56efbc7b --- /dev/null +++ b/Wabbajack/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Wabbajack/Compiler.cs b/Wabbajack/Compiler.cs new file mode 100644 index 00000000..710ecc42 --- /dev/null +++ b/Wabbajack/Compiler.cs @@ -0,0 +1,248 @@ +using Murmur; +using SevenZipExtractor; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Wabbajack.Common; + +namespace Wabbajack +{ + public class Compiler + { + public static HashSet SupportedArchives = new HashSet() { ".zip", ".rar", ".7z", ".7zip" }; + + + public string MO2Folder; + + public dynamic MO2Ini { get; } + public string GamePath { get; } + + public string MO2DownloadsFolder { + get + { + return Path.Combine(MO2Folder, "downloads"); + } + } + + public Action Log_Fn { get; } + public Action Progress_Function { get; } + + public List IndexedArchives; + + public void Info(string msg, params object[] args) + { + if (args.Length > 0) + msg = String.Format(msg, args); + Log_Fn(msg); + } + + public Compiler(string mo2_folder, Action log_fn, Action progress_function) + { + MO2Folder = mo2_folder; + Log_Fn = log_fn; + Progress_Function = progress_function; + MO2Ini = Path.Combine(MO2Folder, "ModOrganizer.ini").LoadIniFile(); + GamePath = ((string)MO2Ini.General.gamePath).Replace("\\\\", "\\"); + } + + public void LoadArchives() + { + IndexedArchives = Directory.EnumerateFiles(MO2DownloadsFolder) + .Where(file => SupportedArchives.Contains(Path.GetExtension(file))) + .AsParallel() + .Select(file => LoadArchive(file)) + .ToList(); + } + + private IndexedArchive LoadArchive(string file) + { + string metaname = file + ".archive_contents"; + + if (metaname.FileExists() && new FileInfo(metaname).LastWriteTime >= new FileInfo(file).LastWriteTime) + { + var info = metaname.FromJSON(); + info.Name = Path.GetFileName(file); + + var ini_name = file + ".meta"; + if (ini_name.FileExists()) + info.IniData = ini_name.LoadIniFile(); + return info; + } + + using (var ar = new ArchiveFile(file)) + { + var streams = new Dictionary(); + ar.Extract(entry => { + if (entry.IsFolder) return null; + + var sha = new SHA256Managed(); + var os = new CryptoStream(Stream.Null, sha, CryptoStreamMode.Write); + streams.Add(entry.FileName, (sha, (long)entry.Size)); + return os; + }); + + var indexed = new IndexedArchiveCache(); + indexed.Hash = file.FileSHA256(); + indexed.Entries = streams.Select(entry => + { + return new IndexedEntry() + { + Hash = entry.Value.Item1.Hash.ToBase64(), + Size = (long)entry.Value.Item2, + Path = entry.Key + }; + }).ToList(); + + streams.Do(e => e.Value.Item1.Dispose()); + + indexed.ToJSON(metaname); + return LoadArchive(file); + } + } + + public void Compile() + { + var mo2_files = Directory.EnumerateFiles(MO2Folder, "*", SearchOption.AllDirectories) + .Where(p => p.FileExists()) + .Select(p => new RawSourceFile() { Path = p.RelativeTo(MO2Folder), AbsolutePath = p }); + + var game_files = Directory.EnumerateFiles(GamePath, "*", SearchOption.AllDirectories) + .Where(p => p.FileExists()) + .Select(p => new RawSourceFile() { Path = Path.Combine(Consts.GameFolderFilesDir, p.RelativeTo(GamePath)), AbsolutePath = p }); + + var all_files = mo2_files.Concat(game_files).ToList(); + + Info("Found {0} files to build into mod list", all_files.Count); + + var stack = MakeStack(); + + var results = all_files.AsParallel().Select(f => RunStack(stack, f)).ToList(); + + var nomatch = results.OfType(); + Info("No match for {0} files", nomatch.Count()); + foreach (var file in nomatch) + Info(" {0}", file.To); + + results.ToJSON("out.json"); + + } + + private Directive RunStack(IEnumerable> stack, RawSourceFile source) + { + return (from f in stack + let result = f(source) + where result != null + select result).First(); + } + + + /// + /// Creates a execution stack. The stack should be passed into Run stack. Each function + /// in this stack will be run in-order and the first to return a non-null result will have its + /// result included into the pack + /// + /// + private IEnumerable> MakeStack() + { + Info("Generating compilation stack"); + return new List>() + { + IgnoreStartsWith("logs\\"), + IgnoreStartsWith("downloads\\"), + IgnoreStartsWith("webcache\\"), + IgnoreEndsWith(".pyc"), + // Ignore the ModOrganizer.ini file it contains info created by MO2 on startup + IgnoreStartsWith("ModOrganizer.ini"), + IgnoreRegex(Consts.GameFolderFilesDir + "\\\\.*\\.bsa"), + DirectMatch(), + DropAll() + }; + } + + private Func IgnoreEndsWith(string v) + { + var reason = String.Format("Ignored because path ends with {0}", v); + return source => + { + if (source.Path.EndsWith(v)) + { + var result = source.EvolveTo(); + result.Reason = reason; + return result; + } + return null; + }; + } + + private Func IgnoreRegex(string p) + { + var reason = String.Format("Ignored because path matches regex {0}", p); + var regex = new Regex(p); + return source => + { + if (regex.IsMatch(source.Path)) + { + var result = source.EvolveTo(); + result.Reason = reason; + return result; + } + return null; + }; + } + + private Func DropAll() + { + return source => { + var result = source.EvolveTo(); + result.Reason = "No Match in Stack"; + return result; + }; + } + + private Func DirectMatch() + { + var indexed = (from archive in IndexedArchives + from entry in archive.Entries + select new { archive = archive, entry = entry }) + .GroupBy(e => e.entry.Hash) + .ToDictionary(e => e.Key); + + return source => + { + Info("Hashing {0}", source.Path); + if (indexed.TryGetValue(source.Hash, out var found)) + { + var result = source.EvolveTo(); + var match = found.FirstOrDefault(f => Path.GetFileName(f.entry.Path) == Path.GetFileName(source.Path)); + if (match == null) + match = found.First(); + + result.ArchiveHash = match.archive.Hash; + result.From = match.entry.Path; + return result; + } + return null; + }; + } + + private Func IgnoreStartsWith(string v) + { + var reason = String.Format("Ignored because path starts with {0}", v); + return source => + { + if (source.Path.StartsWith(v)) + { + var result = source.EvolveTo(); + result.Reason = reason; + return result; + } + return null; + }; + } + } +} diff --git a/Wabbajack/Program.cs b/Wabbajack/Program.cs new file mode 100644 index 00000000..8cbe29a7 --- /dev/null +++ b/Wabbajack/Program.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Wabbajack.Common; + +namespace Wabbajack +{ + class Program + { + static void Main(string[] args) + { + var compiler = new Compiler("c:\\Mod Organizer 2", msg => Console.WriteLine(msg), (msg, id, prog) => Console.WriteLine(msg)); + compiler.LoadArchives(); + compiler.Compile(); + + } + } +} diff --git a/Wabbajack/Properties/AssemblyInfo.cs b/Wabbajack/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..702339de --- /dev/null +++ b/Wabbajack/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Wabbajack")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Wabbajack")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("1649cab3-6f8e-4f2e-844c-931bd4b61233")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Wabbajack/Wabbajack.csproj b/Wabbajack/Wabbajack.csproj new file mode 100644 index 00000000..97dfcb7d --- /dev/null +++ b/Wabbajack/Wabbajack.csproj @@ -0,0 +1,71 @@ + + + + + Debug + AnyCPU + {1649CAB3-6F8E-4F2E-844C-931BD4B61233} + Exe + Wabbajack + Wabbajack + v4.7.2 + 512 + true + true + + + x64 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\murmurhash.1.0.3\lib\net45\MurmurHash.dll + + + + + + + + + + + + + + + + + + + + + + + + + {8aa97f58-5044-4bba-b8d9-a74b6947a660} + SevenZipExtractor + + + {b3f3fb6e-b9eb-4f49-9875-d78578bc7ae5} + Wabbajack.Common + + + + \ No newline at end of file diff --git a/Wabbajack/packages.config b/Wabbajack/packages.config new file mode 100644 index 00000000..94093d76 --- /dev/null +++ b/Wabbajack/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file