53 lines
2.1 KiB
C#
53 lines
2.1 KiB
C#
using System.Formats.Tar;
|
|
using System.IO;
|
|
using ZstdSharp;
|
|
|
|
namespace ParadoxSaveParser.WebAPI;
|
|
|
|
/// <summary>
|
|
/// Uploaded save is a zstd-compressed tar with files of the original save inside.
|
|
/// Game data is stored in entry with name from <see cref="GameSaveEntryNames"/>.
|
|
/// </summary>
|
|
public static class SaveArchive
|
|
{
|
|
public static Task<long> GetGameDataSize(IOPath archivePath, Game game, CancellationToken ct)
|
|
{
|
|
return WithGameDataEntry(archivePath, game, ct,
|
|
entry => Task.FromResult(entry.Length));
|
|
}
|
|
|
|
public static Task ExtractGameData(IOPath archivePath, Game game, Stream dest, long maxBytes,
|
|
CancellationToken ct)
|
|
{
|
|
return WithGameDataEntry(archivePath, game, ct, async entry =>
|
|
{
|
|
if (entry.Length > maxBytes)
|
|
throw new InvalidDataException(
|
|
$"Game data size ({entry.Length} bytes) exceeds limit ({maxBytes} bytes)");
|
|
if (entry.DataStream is not null)
|
|
await entry.DataStream.CopyToAsync(dest, ct);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
private static async Task<T> WithGameDataEntry<T>(IOPath archivePath, Game game, CancellationToken ct,
|
|
Func<TarEntry, Task<T>> action)
|
|
{
|
|
if (!GameSaveEntryNames.Map.TryGetValue(game, out string? entryName))
|
|
throw new ArgumentOutOfRangeException(nameof(game), game, "No game data entry name for game");
|
|
|
|
await using var file = File.OpenRead(archivePath);
|
|
await using var zstd = new DecompressionStream(file);
|
|
await using var tar = new TarReader(zstd);
|
|
while (await tar.GetNextEntryAsync(copyData: false, ct) is { } entry)
|
|
{
|
|
if (entry.EntryType is not (TarEntryType.RegularFile or TarEntryType.V7RegularFile))
|
|
continue;
|
|
if (System.IO.Path.GetFileName(entry.Name) == entryName)
|
|
return await action(entry);
|
|
}
|
|
|
|
throw new InvalidDataException($"Invalid save format: no '{entryName}' entry found");
|
|
}
|
|
}
|