Files
TinfoilVibeServer/TinfoilVibeServer/Services/ROMArchiveReader.cs
T
ecenshu 0e2fec8c01
Build & Push Docker image / build-and-push (push) Successful in 14m38s
ci / build_linux (push) Successful in 4m43s
Skip hashed and same location files
Explicit usings
Multipart rar handling
2025-11-23 21:05:58 +10:30

179 lines
6.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 nonseekable 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 multipart 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 multipart 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 singlefile archive (zip, 7z, singlerar, 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>
/// Backcompat 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);
}
}