// 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);
}
}