using System.Formats.Tar;
using System.IO;
using ZstdSharp;
namespace ParadoxSaveParser.WebAPI;
///
/// Uploaded save is a zstd-compressed tar with files of the original save inside.
/// Game data is stored in entry with name from .
///
public static class SaveArchive
{
public static Task 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 WithGameDataEntry(IOPath archivePath, Game game, CancellationToken ct,
Func> 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");
}
}