// File: Services/RomArchiveReader.cs using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Text.RegularExpressions; using SharpCompress.Archives; using SharpCompress.Archives.Rar; using SharpCompress.Common; using SharpCompress.IO; using SharpCompress.Readers; using TinfoilVibeServer.Utilities; namespace TinfoilVibeServer.Services { /// /// Reads a ROM archive (zip / 7z / rar) from a stream. /// public sealed class RomArchiveReader : IDisposable { private readonly ZipArchive? _zipArchive; private readonly IArchive? _archive; private readonly Stream? _archiveStream; // the stream actually handed to SharpCompress private readonly ICollection? _partStreams; public RomArchiveReader(string path) { _archive = DetectAndWrap(path); } /// /// Opens an archive from a stream. /// The *fileName* parameter is only used to decide which archive format /// to open; it can be null if the caller already knows the format. /// public RomArchiveReader(string path, Stream stream) { if (stream == null) throw new ArgumentNullException(nameof(stream)); var fileInfo = new FileInfo(path); switch (fileInfo.Extension) { case ".zip": // System.IO.Compression can use the stream directly _zipArchive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: false); break; case ".7z": case ".rar": // SharpCompress requires a seekable stream; copy if necessary if (!stream.CanSeek) { var ms = new MemoryStream(); stream.CopyTo(ms); ms.Position = 0; _archiveStream = ms; stream.Dispose(); // original non‑seekable stream no longer needed } else { _archiveStream = stream; } _archive = ArchiveFactory.Open(_archiveStream, new ReaderOptions() { LeaveStreamOpen = false, BufferSize = 10 * 1024 * 1024}); break; default: throw new NotSupportedException($"Archive type '{fileInfo.Extension}' is not supported."); } } // Detect whether the file is a multi‑part RAR and wrap it if necessary private static IArchive DetectAndWrap(string path) { string ext = Path.GetExtension(path).ToLowerInvariant(); if (ext == ".rar" || ext == ".r00" || ext == ".r01" || ext == ".r02") { var dir = Path.GetDirectoryName(path)!; var fileName = Path.GetFileName(path); // ----- 1️⃣ Determine the base name (everything before the first ".rar" or ".partNN") ----- string baseName = MultiPartRarHelper.GetBaseNameForRarVolume(fileName); // Any file that ends with .rar or .rNN could be the start of a multi‑part set // Let MultiPartRarStream decide which parts belong together. var volumes = MultiPartRarHelper.DiscoverVolumes(dir, baseName); if (volumes.Count is 0 or 1) { return ArchiveFactory.Open(path); } var streams = new List(volumes.Count); foreach (var volume in volumes) { streams.Add(new FileStream(volume, FileMode.Open, FileAccess.Read, FileShare.Read)); } return ArchiveFactory.Open(streams, new ReaderOptions { LeaveStreamOpen = false }); } // Normal single‑file archive (zip, 7z, single‑rar, etc.) using var archiveStream = File.OpenRead(path); return ArchiveFactory.Open(archiveStream); } private static Stream? GetPart(int arg) { throw new NotImplementedException(); } public Stream? GetEntryStream(string entryName) { var entry = _archive?.Entries .Single(e => e.Key != null && e.Key.Equals(entryName, StringComparison.OrdinalIgnoreCase)); return entry?.OpenEntryStream(); } /// /// Enumerates every file entry inside the archive. /// public IEnumerable GetEntries() { if (_zipArchive != null) { foreach (var entry in _zipArchive.Entries) { if (entry.FullName.EndsWith("/", StringComparison.Ordinal)) continue; // skip directories yield return new RomArchiveEntry(entry.FullName, entry.Open()); } } else if (_archive != null) { foreach (var entry in _archive.Entries) { if (entry.IsDirectory) continue; // SharpCompress gives us a stream that must be disposed by the caller if (!entry.Archive.IsComplete) { if (entry.Key != null) { var entryStream = GetEntryStream(entry.Key); if (entryStream != null) yield return new RomArchiveEntry(entry.Key, entryStream); } } else { if (entry.Key != null) yield return new RomArchiveEntry(entry.Key, entry.OpenEntryStream()); } } } } /// /// Back‑compat wrapper used by SnapshotService. /// public IEnumerable GetContentInfos() => GetEntries(); /// /// Disposes the underlying archive objects and the stream(s). /// public void Dispose() { _zipArchive?.Dispose(); _archive?.Dispose(); _archiveStream?.Dispose(); } /// /// Lightweight container that holds the entry name and the opened stream. /// The caller must dispose Stream after it is done. /// public sealed record RomArchiveEntry(string Name, Stream Stream); } }