diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj
index 144b7643..0d2b1d12 100644
--- a/ArcFormats/ArcFormats.csproj
+++ b/ArcFormats/ArcFormats.csproj
@@ -160,6 +160,8 @@
+
+
@@ -1272,12 +1274,18 @@
PreserveNewest
+
+ PreserveNewest
+
PreserveNewest
PreserveNewest
+
+ PreserveNewest
+
PreserveNewest
diff --git a/ArcFormats/UnArr.cs b/ArcFormats/UnArr.cs
new file mode 100644
index 00000000..cc227eed
--- /dev/null
+++ b/ArcFormats/UnArr.cs
@@ -0,0 +1,199 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+using System.Runtime.InteropServices.ComTypes;
+
+namespace GameRes.Formats {
+ ///
+ /// A tool to unpack zip/rar/7z/tar archives.
+ ///
+ public class UnArr {
+ [ComVisible(true)]
+ [ClassInterface(ClassInterfaceType.None)]
+ public class StreamWrapper : IStream
+ {
+ private readonly Stream m_stream;
+
+ public StreamWrapper(Stream stream)
+ {
+ m_stream = stream ?? throw new ArgumentNullException(nameof(stream));
+ }
+
+ public void Read(byte[] pv, int cb, IntPtr pcbRead)
+ {
+ int bytesRead = m_stream.Read(pv, 0, cb);
+ if (pcbRead != IntPtr.Zero)
+ {
+ Marshal.WriteInt32(pcbRead, bytesRead);
+ }
+ }
+
+ public void Write(byte[] pv, int cb, IntPtr pcbWritten)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void Seek(long dlibMove, int dwOrigin, IntPtr plibNewPosition)
+ {
+ long newPosition = m_stream.Seek(dlibMove, (SeekOrigin)dwOrigin);
+ if (plibNewPosition != IntPtr.Zero)
+ {
+ Marshal.WriteInt64(plibNewPosition, newPosition);
+ }
+ }
+
+ public void SetSize(long libNewSize)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void CopyTo(IStream pstm, long cb, IntPtr pcbRead, IntPtr pcbWritten)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void Commit(int grfCommitFlags)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void Revert()
+ {
+ throw new NotSupportedException();
+ }
+
+ public void LockRegion(long libOffset, long cb, int dwLockType)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void UnlockRegion(long libOffset, long cb, int dwLockType)
+ {
+ throw new NotSupportedException();
+ }
+
+ public void Stat(out System.Runtime.InteropServices.ComTypes.STATSTG pstatstg, int grfStatFlag)
+ {
+ pstatstg = new System.Runtime.InteropServices.ComTypes.STATSTG();
+ pstatstg.type = 2; // STGTY_STREAM
+ pstatstg.cbSize = m_stream.Length;
+ }
+
+ public void Clone(out IStream ppstm)
+ {
+ ppstm = null;
+ throw new NotSupportedException();
+ }
+ }
+
+ [DllImport("unarr.dll", CallingConvention = CallingConvention.Cdecl)]
+ // opens a read-only stream based on the given IStream
+ // ar_stream *ar_open_istream(IStream *stream);
+ public static extern IntPtr ar_open_istream([In, MarshalAs(UnmanagedType.Interface)] IStream stream);
+ [DllImport("unarr.dll", CallingConvention = CallingConvention.Cdecl)]
+ // closes the stream and releases underlying resources
+ // void ar_close(ar_stream *stream);
+ public static extern void ar_close(IntPtr stream);
+ [DllImport("unarr.dll", CallingConvention = CallingConvention.Cdecl)]
+ // tries to read 'count' bytes into buffer, advancing the read offset pointer; returns the actual number of bytes read
+ // size_t ar_read(ar_stream *stream, void *buffer, size_t count);
+ public static extern UIntPtr ar_read(IntPtr stream, IntPtr buffer, UIntPtr count);
+ [DllImport("unarr.dll", CallingConvention = CallingConvention.Cdecl)]
+ [return: MarshalAs(UnmanagedType.U1)]
+ // moves the read offset pointer (same as fseek); returns false on failure
+ // bool ar_seek(ar_stream *stream, off64_t offset, int origin);
+ public static extern bool ar_seek(IntPtr stream, long offset, int origin);
+ [DllImport("unarr.dll", CallingConvention = CallingConvention.Cdecl)]
+ [return: MarshalAs(UnmanagedType.U1)]
+ // shortcut for ar_seek(stream, count, SEEK_CUR); returns false on failure
+ // bool ar_skip(ar_stream *stream, off64_t count);
+ public static extern bool ar_skip(IntPtr stream, long count);
+ [DllImport("unarr.dll", CallingConvention = CallingConvention.Cdecl)]
+ // returns the current read offset (or 0 on error)
+ // off64_t ar_tell(ar_stream *stream);
+ public static extern long ar_tell(IntPtr stream);
+ [DllImport("unarr.dll", CallingConvention = CallingConvention.Cdecl)]
+ // frees all data stored for the given archive; does not close the underlying stream
+ // void ar_close_archive(ar_archive *ar);
+ public static extern void ar_close_archive(IntPtr ar);
+ [DllImport("unarr.dll", CallingConvention = CallingConvention.Cdecl)]
+ // reads the next archive entry; returns false on error or at the end of the file (use ar_at_eof to distinguish the two cases)
+ // bool ar_parse_entry(ar_archive *ar);
+ [return: MarshalAs(UnmanagedType.U1)]
+ public static extern bool ar_parse_entry(IntPtr ar);
+ [DllImport("unarr.dll", CallingConvention = CallingConvention.Cdecl)]
+ // reads the archive entry at the given offset as returned by ar_entry_get_offset (offset 0 always restarts at the first entry); should always succeed
+ // bool ar_parse_entry_at(ar_archive *ar, off64_t offset)
+ [return: MarshalAs(UnmanagedType.U1)]
+ public static extern bool ar_parse_entry_at(IntPtr ar, long offset);
+ [DllImport("unarr.dll", CallingConvention = CallingConvention.Cdecl)]
+ // reads the (first) archive entry associated with the given name; returns false if the entry couldn't be found
+ // entry_name should be UTF-8 encoded
+ // bool ar_parse_entry_for(ar_archive *ar, const char *entry_name);
+ [return: MarshalAs(UnmanagedType.U1)]
+ public static extern bool ar_parse_entry_for(IntPtr ar, IntPtr entry_name);
+ public static bool ArParseEntryFor(IntPtr ar, string entry_name) {
+ var bytes = Encoding.UTF8.GetBytes(entry_name);
+ var ptr = Marshal.AllocHGlobal(bytes.Length + 1);
+ Marshal.Copy(bytes, 0, ptr, bytes.Length);
+ Marshal.WriteByte(ptr, bytes.Length, 0);
+ var result = ar_parse_entry_for(ar, ptr);
+ Marshal.FreeHGlobal(ptr);
+ return result;
+ }
+ [DllImport("unarr.dll", CallingConvention = CallingConvention.Cdecl)]
+ // returns whether the last ar_parse_entry call has reached the file's expected end
+ // bool ar_at_eof(ar_archive *ar);
+ [return: MarshalAs(UnmanagedType.U1)]
+ public static extern bool ar_at_eof(IntPtr ar);
+ [DllImport("unarr.dll", CallingConvention = CallingConvention.Cdecl)]
+ // returns the name of the current entry as UTF-8 string; this pointer is only valid until the next call to ar_parse_entry; returns NULL on failure
+ // const char *ar_entry_get_name(ar_archive *ar);
+ public static extern IntPtr ar_entry_get_name(IntPtr ar);
+ public static string ArEntryGetName(IntPtr ar) {
+ var ptr = ar_entry_get_name(ar);
+ if (ptr == IntPtr.Zero)
+ return null;
+ int len = 0;
+ while (Marshal.ReadByte(ptr, len) != 0) ++len;
+ byte[] buffer = new byte[len];
+ Marshal.Copy(ptr, buffer, 0, len);
+ return Encoding.UTF8.GetString(buffer);
+ }
+ [DllImport("unarr.dll", CallingConvention = CallingConvention.Cdecl)]
+ // returns the stream offset of the current entry for use with ar_parse_entry_at
+ // off64_t ar_entry_get_offset(ar_archive *ar);
+ public static extern long ar_entry_get_offset(IntPtr ar);
+ [DllImport("unarr.dll", CallingConvention = CallingConvention.Cdecl)]
+ // returns the total size of uncompressed data of the current entry; read exactly that many bytes using ar_entry_uncompress
+ // size_t ar_entry_get_size(ar_archive *ar);
+ public static extern UIntPtr ar_entry_get_size(IntPtr ar);
+ [DllImport("unarr.dll", CallingConvention = CallingConvention.Cdecl)]
+ // WARNING: don't manually seek in the stream between ar_parse_entry and the last corresponding ar_entry_uncompress call!
+ // uncompresses the next 'count' bytes of the current entry into buffer; returns false on error
+ // bool ar_entry_uncompress(ar_archive *ar, void *buffer, size_t count);
+ [return: MarshalAs(UnmanagedType.U1)]
+ public static extern bool ar_entry_uncompress(IntPtr ar, IntPtr buffer, UIntPtr count);
+ [DllImport("unarr.dll", CallingConvention = CallingConvention.Cdecl)]
+ // checks whether 'stream' could contain RAR data and prepares for archive listing/extraction; returns NULL on failure
+ // ar_archive *ar_open_rar_archive(ar_stream *stream);
+ public static extern IntPtr ar_open_rar_archive(IntPtr stream);
+ [DllImport("unarr.dll", CallingConvention = CallingConvention.Cdecl)]
+ // checks whether 'stream' could contain TAR data and prepares for archive listing/extraction; returns NULL on failure
+ // ar_archive *ar_open_tar_archive(ar_stream *stream);
+ public static extern IntPtr ar_open_tar_archive(IntPtr stream);
+ [DllImport("unarr.dll", CallingConvention = CallingConvention.Cdecl)]
+ // checks whether 'stream' could contain ZIP data and prepares for archive listing/extraction; returns NULL on failure
+ // set deflatedonly for extracting XPS, EPUB, etc. documents where non-Deflate compression methods are not supported by specification
+ // ar_archive *ar_open_zip_archive(ar_stream *stream, bool deflatedonly);
+ public static extern IntPtr ar_open_zip_archive(IntPtr stream, [MarshalAs(UnmanagedType.U1)] bool deflatedonly);
+ [DllImport("unarr.dll", CallingConvention = CallingConvention.Cdecl)]
+ // checks whether 'stream' could contain 7Z data and prepares for archive listing/extraction; returns NULL on failure
+ // ar_archive *ar_open_7z_archive(ar_stream *stream);
+ public static extern IntPtr ar_open_7z_archive(IntPtr stream);
+ }
+}
diff --git a/ArcFormats/UnArrArchive.cs b/ArcFormats/UnArrArchive.cs
new file mode 100644
index 00000000..6f275e20
--- /dev/null
+++ b/ArcFormats/UnArrArchive.cs
@@ -0,0 +1,224 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.Composition;
+using System.IO;
+using System.Runtime.InteropServices;
+using GameRes;
+using GameRes.Formats;
+
+namespace GameRes.Formats
+{
+ ///
+ /// UnArr archive file wrapper.
+ ///
+ public class UnArrArcFile : ArcFile
+ {
+ public IntPtr ArStream { get; private set; }
+ public IntPtr ArArchive { get; private set; }
+
+ public UnArrArcFile(ArcView arc, ArchiveFormat impl, ICollection dir, IntPtr arStream, IntPtr arArchive)
+ : base(arc, impl, dir)
+ {
+ ArStream = arStream;
+ ArArchive = arArchive;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (ArArchive != IntPtr.Zero)
+ {
+ UnArr.ar_close_archive(ArArchive);
+ ArArchive = IntPtr.Zero;
+ }
+ if (ArStream != IntPtr.Zero)
+ {
+ UnArr.ar_close(ArStream);
+ ArStream = IntPtr.Zero;
+ }
+ base.Dispose(disposing);
+ }
+ }
+
+ ///
+ /// Base class for UnArr archive formats.
+ ///
+ public abstract class UnArrBaseArchive : ArchiveFormat
+ {
+ public override bool IsHierarchic { get { return true; } }
+
+ public override ArcFile TryOpen(ArcView file)
+ {
+ var stream = file.CreateStream();
+ var streamWrapper = new UnArr.StreamWrapper(stream);
+ IntPtr arStream = IntPtr.Zero;
+ IntPtr arArchive = IntPtr.Zero;
+
+ try
+ {
+ arStream = UnArr.ar_open_istream(streamWrapper);
+ if (arStream == IntPtr.Zero)
+ return null;
+
+ arArchive = OpenArchive(arStream);
+ if (arArchive == IntPtr.Zero)
+ return null;
+
+ var dir = new List();
+ long offset = 0;
+
+ while (UnArr.ar_parse_entry(arArchive))
+ {
+ string name = UnArr.ArEntryGetName(arArchive);
+ if (string.IsNullOrEmpty(name))
+ continue;
+
+ var entry = new Entry
+ {
+ Name = name,
+ Type = FormatCatalog.Instance.GetTypeFromName(name, ContainedFormats),
+ Offset = offset,
+ Size = (uint)UnArr.ar_entry_get_size(arArchive).ToUInt64()
+ };
+ dir.Add(entry);
+ offset = UnArr.ar_entry_get_offset(arArchive);
+ }
+
+ if (dir.Count == 0)
+ return null;
+
+ return new UnArrArcFile(file, this, dir, arStream, arArchive);
+ }
+ catch
+ {
+ if (arArchive != IntPtr.Zero)
+ UnArr.ar_close_archive(arArchive);
+ if (arStream != IntPtr.Zero)
+ UnArr.ar_close(arStream);
+ throw;
+ }
+ }
+
+ ///
+ /// Open archive from ar_stream. Must be implemented by derived classes.
+ ///
+ protected abstract IntPtr OpenArchive(IntPtr arStream);
+
+ public override Stream OpenEntry(ArcFile arc, Entry entry)
+ {
+ var unarc = arc as UnArrArcFile;
+ if (unarc == null)
+ return base.OpenEntry(arc, entry);
+
+ if (!UnArr.ar_parse_entry_at(unarc.ArArchive, entry.Offset))
+ return Stream.Null;
+
+ var size = UnArr.ar_entry_get_size(unarc.ArArchive);
+ var oriSize = size.ToUInt64();
+
+ var buffer = Marshal.AllocHGlobal((int)oriSize);
+ try
+ {
+ if (!UnArr.ar_entry_uncompress(unarc.ArArchive, buffer, size))
+ {
+ Marshal.FreeHGlobal(buffer);
+ return Stream.Null;
+ }
+
+ byte[] data = new byte[oriSize];
+ Marshal.Copy(buffer, data, 0, (int)oriSize);
+ Marshal.FreeHGlobal(buffer);
+
+ return new MemoryStream(data, false);
+ }
+ catch
+ {
+ Marshal.FreeHGlobal(buffer);
+ throw;
+ }
+ }
+ }
+
+ ///
+ /// 7-Zip archive format implementation.
+ ///
+ [Export(typeof(ArchiveFormat))]
+ public class SevenZipArchive : UnArrBaseArchive
+ {
+ public override string Tag { get { return "7Z"; } }
+ public override string Description { get { return "7-Zip archive"; } }
+ public override uint Signature { get { return 0; } }
+ public override bool IsHierarchic { get { return true; } }
+ public override bool CanWrite { get { return false; } }
+
+ protected override IntPtr OpenArchive(IntPtr arStream)
+ {
+ return UnArr.ar_open_7z_archive(arStream);
+ }
+ }
+
+ ///
+ /// RAR archive format implementation.
+ ///
+ [Export(typeof(ArchiveFormat))]
+ public class RarArchive : UnArrBaseArchive
+ {
+ public override string Tag { get { return "RAR"; } }
+ public override string Description { get { return "RAR archive"; } }
+ public override uint Signature { get { return 0; } } // 'Rar!'
+ public override bool IsHierarchic { get { return true; } }
+ public override bool CanWrite { get { return false; } }
+
+ protected override IntPtr OpenArchive(IntPtr arStream)
+ {
+ return UnArr.ar_open_rar_archive(arStream);
+ }
+ }
+
+ ///
+ /// TAR archive format implementation.
+ ///
+ [Export(typeof(ArchiveFormat))]
+ public class TarArchive : UnArrBaseArchive
+ {
+ public override string Tag { get { return "TAR"; } }
+ public override string Description { get { return "TAR archive"; } }
+ public override uint Signature { get { return 0; } } // TAR没有固定的魔术数字
+ public override bool IsHierarchic { get { return true; } }
+ public override bool CanWrite { get { return false; } }
+
+ protected override IntPtr OpenArchive(IntPtr arStream)
+ {
+ return UnArr.ar_open_tar_archive(arStream);
+ }
+
+ public override ArcFile TryOpen(ArcView file)
+ {
+ // TAR文件通常在偏移257处有"ustar"标识
+ if (file.MaxOffset > 0x105)
+ {
+ var signature = file.View.ReadBytes(0x101, 5);
+ if (signature != null && System.Text.Encoding.ASCII.GetString(signature) == "ustar")
+ return base.TryOpen(file);
+ }
+ return null;
+ }
+ }
+
+ ///
+ /// ZIP archive format implementation.
+ ///
+ // [Export(typeof(ArchiveFormat))]
+ // public class ZipArchive : UnArrBaseArchive
+ // {
+ // public override string Tag { get { return "ZIP/UnArr"; } }
+ // public override string Description { get { return "ZIP archive (UnArr)"; } }
+ // public override uint Signature { get { return 0x04034b50; } } // PK\x03\x04
+ // public override bool IsHierarchic { get { return true; } }
+ // public override bool CanWrite { get { return false; } }
+
+ // protected override IntPtr OpenArchive(IntPtr arStream)
+ // {
+ // return UnArr.ar_open_zip_archive(arStream, false);
+ // }
+ // }
+}
diff --git a/ArcFormats/x64/unarr.dll b/ArcFormats/x64/unarr.dll
new file mode 100644
index 00000000..c6a0e583
Binary files /dev/null and b/ArcFormats/x64/unarr.dll differ
diff --git a/ArcFormats/x86/unarr.dll b/ArcFormats/x86/unarr.dll
new file mode 100644
index 00000000..abbe6b1a
Binary files /dev/null and b/ArcFormats/x86/unarr.dll differ