Add 7z/tar/rar support via unarr

This commit is contained in:
2025-10-15 14:20:26 +08:00
parent 3446a003e9
commit 9251d1e080
5 changed files with 431 additions and 0 deletions

View File

@@ -160,6 +160,8 @@
<Compile Include="Software House Parsley\ArcCG3.cs" />
<Compile Include="Software House Parsley\ArcScn.cs" />
<Compile Include="TechGian\ArcBIN.cs" />
<Compile Include="UnArr.cs" />
<Compile Include="UnArrArchive.cs" />
<Compile Include="Unity\ScriptDSM.cs" />
<Compile Include="Xuse\ArcNT.cs" />
<Compile Include="Xuse\ArcWVB.cs" />
@@ -1272,12 +1274,18 @@
<Content Include="x64\jxl_dec.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="x64\unarr.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="x64\zstd-1.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="x86\jxl_dec.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="x86\unarr.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="x86\zstd-1.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>

199
ArcFormats/UnArr.cs Normal file
View File

@@ -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 {
/// <summary>
/// A tool to unpack zip/rar/7z/tar archives.
/// </summary>
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);
}
}

224
ArcFormats/UnArrArchive.cs Normal file
View File

@@ -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
{
/// <summary>
/// UnArr archive file wrapper.
/// </summary>
public class UnArrArcFile : ArcFile
{
public IntPtr ArStream { get; private set; }
public IntPtr ArArchive { get; private set; }
public UnArrArcFile(ArcView arc, ArchiveFormat impl, ICollection<Entry> 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);
}
}
/// <summary>
/// Base class for UnArr archive formats.
/// </summary>
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<Entry>();
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;
}
}
/// <summary>
/// Open archive from ar_stream. Must be implemented by derived classes.
/// </summary>
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;
}
}
}
/// <summary>
/// 7-Zip archive format implementation.
/// </summary>
[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);
}
}
/// <summary>
/// RAR archive format implementation.
/// </summary>
[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);
}
}
/// <summary>
/// TAR archive format implementation.
/// </summary>
[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;
}
}
/// <summary>
/// ZIP archive format implementation.
/// </summary>
// [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);
// }
// }
}

BIN
ArcFormats/x64/unarr.dll Normal file
View File

Binary file not shown.

BIN
ArcFormats/x86/unarr.dll Normal file
View File

Binary file not shown.