179 lines
6.7 KiB
C#
179 lines
6.7 KiB
C#
// 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
|
||
{
|
||
/// <summary>
|
||
/// Reads a ROM archive (zip / 7z / rar) from a stream.
|
||
/// </summary>
|
||
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<Stream>? _partStreams;
|
||
|
||
public RomArchiveReader(string path)
|
||
{
|
||
_archive = DetectAndWrap(path);
|
||
}
|
||
/// <summary>
|
||
/// Opens an archive from a stream.
|
||
/// The *fileName* parameter is only used to decide which archive format
|
||
/// to open; it can be <c>null</c> if the caller already knows the format.
|
||
/// </summary>
|
||
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<Stream>(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();
|
||
}
|
||
/// <summary>
|
||
/// Enumerates every file entry inside the archive.
|
||
/// </summary>
|
||
public IEnumerable<RomArchiveEntry> 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());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Back‑compat wrapper used by SnapshotService.
|
||
/// </summary>
|
||
public IEnumerable<RomArchiveEntry> GetContentInfos() => GetEntries();
|
||
|
||
/// <summary>
|
||
/// Disposes the underlying archive objects and the stream(s).
|
||
/// </summary>
|
||
public void Dispose()
|
||
{
|
||
_zipArchive?.Dispose();
|
||
_archive?.Dispose();
|
||
_archiveStream?.Dispose();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Lightweight container that holds the entry name and the opened stream.
|
||
/// The caller must dispose <c>Stream</c> after it is done.
|
||
/// </summary>
|
||
public sealed record RomArchiveEntry(string Name, Stream Stream);
|
||
}
|
||
} |