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