diff --git a/Directory.Packages.props b/Directory.Packages.props index a6e9f40d89c..c1b4f1a19b0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,6 +6,7 @@ + diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 00000000000..282ecf659e8 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/References/LibArchive.Net.0.2.0-alpha.0.82.nupkg b/References/LibArchive.Net.0.2.0-alpha.0.82.nupkg new file mode 100644 index 00000000000..1f16759114d Binary files /dev/null and b/References/LibArchive.Net.0.2.0-alpha.0.82.nupkg differ diff --git a/src/BizHawk.Common/BizHawk.Common.csproj b/src/BizHawk.Common/BizHawk.Common.csproj index a110093bce4..967ca162778 100644 --- a/src/BizHawk.Common/BizHawk.Common.csproj +++ b/src/BizHawk.Common/BizHawk.Common.csproj @@ -15,6 +15,7 @@ + diff --git a/src/BizHawk.Common/LibarchiveArchiveFile.cs b/src/BizHawk.Common/LibarchiveArchiveFile.cs new file mode 100644 index 00000000000..b5c68b90e0e --- /dev/null +++ b/src/BizHawk.Common/LibarchiveArchiveFile.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using BizHawk.Common; + +using LibArchive.Net; + +namespace BizHawk.Client.Common +{ + /// + public sealed class LibarchiveArchiveFile : IHawkArchiveFile + { + private LibArchiveReader? _handle; + + private FileInfo? _tempOnDisk; + + private (int ArchiveIndex, LibArchiveReader.Entry Entry)[]? field = null; + + private (int ArchiveIndex, LibArchiveReader.Entry Entry)[] AllEntries + => field ??= _handle?.Entries().Index().OrderBy(static tuple => tuple.Item.Name).ToArray() + ?? throw new ObjectDisposedException(nameof(LibarchiveArchiveFile)); + + public LibarchiveArchiveFile(string path) + => _handle = new(path); + + public LibarchiveArchiveFile(Stream fileStream) + { + _tempOnDisk = new(TempFileManager.GetTempFilename("dearchive")); + using (FileStream fsCopy = new(_tempOnDisk.FullName, FileMode.Create)) fileStream.CopyTo(fsCopy); + try + { + _handle = new(_tempOnDisk.FullName); + } + catch (Exception e) + { + _tempOnDisk.Delete(); + Console.WriteLine(e); + throw; + } + } + + public void Dispose() + { + _handle?.Dispose(); + _handle = null; + _tempOnDisk?.Delete(); + _tempOnDisk = null; + } + + public void ExtractFile(int index, Stream stream) + { + using var entryStream = AllEntries[index].Entry.Stream; + entryStream.CopyTo(stream); + } + + public List? Scan() + => AllEntries.Select(static (tuple, i) => new HawkArchiveFileItem( + tuple.Entry.Name, + size: tuple.Entry.LengthBytes ?? 0, + index: i, + archiveIndex: tuple.ArchiveIndex)).ToList(); + } +} diff --git a/src/BizHawk.Common/LibarchiveDearchivalMethod.cs b/src/BizHawk.Common/LibarchiveDearchivalMethod.cs new file mode 100644 index 00000000000..9e8ba53c784 --- /dev/null +++ b/src/BizHawk.Common/LibarchiveDearchivalMethod.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.IO; + +using BizHawk.Common; + +namespace BizHawk.Client.Common +{ + /// A dearchival method for implemented using libarchive (via LibArchive.Net bindings). + public class LibarchiveDearchivalMethod : IFileDearchivalMethod + { + public static readonly LibarchiveDearchivalMethod Instance = new(); + + public IReadOnlyCollection AllowedArchiveExtensions { get; } = [ + ".7z", + ".gz", + ".rar", + ".tar", + /*.tar*/".bz2", ".tb2", ".tbz", ".tbz2", ".tz2", + /*.tar.gz,*/ ".taz", ".tgz", + /*.tar*/".lz", + ".zip", + ]; + + private LibarchiveDearchivalMethod() {} + + public bool CheckSignature(string fileName) + { + LibarchiveArchiveFile? file = null; + try + { + file = new(fileName); + return true; + } + catch (Exception) + { + return false; + } + finally + { + file?.Dispose(); + } + } + + public bool CheckSignature(Stream fileStream, string? filenameHint) + { + if (!fileStream.CanRead || !fileStream.CanSeek) return false; + var initialPosition = fileStream.Position; + LibarchiveArchiveFile? file = null; + try + { + file = new(fileStream); + return true; + } + catch (Exception) + { + return false; + } + finally + { + file?.Dispose(); + fileStream.Seek(initialPosition, SeekOrigin.Begin); + } + } + + public LibarchiveArchiveFile Construct(string path) + => new(path); + + public LibarchiveArchiveFile Construct(Stream fileStream) + => new(fileStream); + } +} diff --git a/src/BizHawk.Tests.Client.Common/dearchive/DearchivalTests.cs b/src/BizHawk.Tests.Client.Common/dearchive/DearchivalTests.cs index 7526d88fef9..1d6aae04ca8 100644 --- a/src/BizHawk.Tests.Client.Common/dearchive/DearchivalTests.cs +++ b/src/BizHawk.Tests.Client.Common/dearchive/DearchivalTests.cs @@ -1,5 +1,5 @@ -using System.IO; -using System.Linq; +using System.Collections.Generic; +using System.IO; using BizHawk.Client.Common; using BizHawk.Common; @@ -12,13 +12,14 @@ public class DearchivalTests { private const string EMBED_GROUP = "data.dearchive."; - private static readonly (string Filename, bool HasSharpCompressSupport)[] TestCases = { - ("m3_scy_change.7z", true), - ("m3_scy_change.gb.gz", true), - ("m3_scy_change.rar", true), - ("m3_scy_change.bsdtar.tar", true), - ("m3_scy_change.gnutar.tar", true), - ("m3_scy_change.zip", true), + private static IEnumerable TestCases { get; } = new[] + { + new object?[] { "m3_scy_change.7z", true }, + new object?[] { "m3_scy_change.gb.gz", true }, + new object?[] { "m3_scy_change.rar", true }, + new object?[] { "m3_scy_change.bsdtar.tar", true }, + new object?[] { "m3_scy_change.gnutar.tar", true }, + new object?[] { "m3_scy_change.zip", true }, }; private readonly Lazy _rom = new(static () => ReflectionCache.EmbeddedResourceStream(EMBED_GROUP + "m3_scy_change.gb").ReadAllBytes()); @@ -30,16 +31,33 @@ private static readonly (string Filename, bool HasSharpCompressSupport)[] TestCa public void SanityCheck() => Assert.AreEqual("SHA1:70DCA8E791878BDD32426391E4233EA52B47CDD1", SHA1Checksum.ComputePrefixedHex(Rom)); #pragma warning restore BHI1600 + [DynamicData(nameof(TestCases))] + [TestMethod] + public void TestLibarchive(string filename, bool hasSharpCompressSupport) + { + var sc = LibarchiveDearchivalMethod.Instance; + var archive = ReflectionCache.EmbeddedResourceStream(EMBED_GROUP + filename); + Assert.IsTrue(sc.CheckSignature(archive, filename), $"{filename} is an archive, but wasn't detected as such"); // puts the seek pos of the Stream param back where it was (in this case at the start) + using var af = sc.Construct(archive); + var items = af.Scan(); + Assert.IsNotNull(items, $"{filename} contains 1 file, but it couldn't be enumerated correctly"); + Assert.AreEqual(1, items!.Count, $"{filename} contains 1 file, but was detected as containing {items.Count} files"); + using MemoryStream ms = new((int) items[0].Size); + af.ExtractFile(items[0].ArchiveIndex, ms); + ms.Seek(0L, SeekOrigin.Begin); + CollectionAssert.AreEqual(Rom, ms.ReadAllBytes(), $"the file extracted from {filename} doesn't match the uncompressed file"); + } + + [DynamicData(nameof(TestCases))] [TestMethod] - public void TestSharpCompress() + public void TestSharpCompress(string filename, bool hasSharpCompressSupport) { + if (!hasSharpCompressSupport) return; var sc = SharpCompressDearchivalMethod.Instance; - foreach (var filename in TestCases.Where(testCase => testCase.HasSharpCompressSupport) - .Select(testCase => testCase.Filename)) - { + /*scope*/{ var archive = ReflectionCache.EmbeddedResourceStream(EMBED_GROUP + filename); Assert.IsTrue(sc.CheckSignature(archive, filename), $"{filename} is an archive, but wasn't detected as such"); // puts the seek pos of the Stream param back where it was (in this case at the start) - var af = sc.Construct(archive); + using var af = sc.Construct(archive); var items = af.Scan(); Assert.IsNotNull(items, $"{filename} contains 1 file, but it couldn't be enumerated correctly"); Assert.AreEqual(1, items!.Count, $"{filename} contains 1 file, but was detected as containing {items.Count} files");