diff --git a/ParadoxSaveParser.Lib/Parser.cs b/ParadoxSaveParser.Lib/Parser.cs index c52751c..ae85609 100644 --- a/ParadoxSaveParser.Lib/Parser.cs +++ b/ParadoxSaveParser.Lib/Parser.cs @@ -1,29 +1,190 @@ -namespace ParadoxSaveParser.Lib; +using System.Text; -public abstract class Parser +namespace ParadoxSaveParser.Lib; + +public class Parser { protected Stream _saveFile; - - protected Parser(Stream savefile) + private List _tokens = new(); + private int _tokenIndex; + + public Parser(Stream savefile) { _saveFile = savefile; } - + protected enum TokenType { - Invalid, String, Equals, BracketOpen, BracketClose, + Invalid, + String, + Equals, + BracketOpen, + BracketClose, } - + protected struct Token { public TokenType type; public string? value; + + public override string ToString() + { + switch (type) + { + case TokenType.Invalid: + return "INVALID_TOKEN"; + case TokenType.String: + return value ?? "NULL"; + case TokenType.Equals: + return "="; + case TokenType.BracketOpen: + return "{"; + case TokenType.BracketClose: + return "}"; + default: + throw new ArgumentOutOfRangeException(type.ToString()); + } + } } - - protected void BuildAST() + + protected void Lex() { - + _tokens.Clear(); + StringBuilder str = new(); + + void CompleteStringToken() + { + if (str.Length > 0 && str[0] != '#') + { + _tokens.Add(new Token { type = TokenType.String, value = str.ToString() }); + str.Clear(); + } + } + + while (_saveFile.CanRead) + { + int c = _saveFile.ReadByte(); + switch (c) + { + case -1: + CompleteStringToken(); + return; + case ' ': + case '\t': + case '\n': + case '\r': + CompleteStringToken(); + break; + case '=': + CompleteStringToken(); + _tokens.Add(new Token { type = TokenType.Equals }); + break; + case '{': + CompleteStringToken(); + _tokens.Add(new Token { type = TokenType.BracketOpen }); + break; + case '}': + CompleteStringToken(); + _tokens.Add(new Token { type = TokenType.BracketClose }); + break; + default: + str.Append((char)c); + break; + } + } + } + + protected class UnexpectedTokenException : Exception + { + public UnexpectedTokenException(Token token, int tokenIndex) : + base($"Unexpected token at index {tokenIndex}: {token}") + {} } - public abstract SaveData Parse(); + + private object? ParseValue() + { + Token tok = _tokens[_tokenIndex++]; + switch (tok.type) + { + case TokenType.String: + return tok.value!; + case TokenType.BracketOpen: + return ParseListOrDict(); + case TokenType.BracketClose: + return null; + default: + throw new UnexpectedTokenException(tok, _tokenIndex - 1); + } + } + + private object ParseListOrDict() + { + Token first = _tokens[_tokenIndex]; + Token second = _tokens[_tokenIndex + 1]; + if (first.type == TokenType.String && second.type == TokenType.Equals) + return ParseDict(); + + return ParseList(); + } + + private List ParseList() + { + List list = new(); + Token tok = _tokens[_tokenIndex]; + while (tok.type != TokenType.BracketClose) + { + object? value = ParseValue(); + if (value == null) + break; + list.Add(value); + } + return list; + } + + + private Dictionary> ParseDict() + { + Dictionary> dict = new(); + // root is a dict without closing bracket, so this method must check _tokenIndex < _tokens.Count + while (_tokenIndex < _tokens.Count) + { + Token tok = _tokens[_tokenIndex++]; + if (tok.type == TokenType.BracketClose) + break; + + if(tok.type != TokenType.String) + throw new UnexpectedTokenException(tok, _tokenIndex - 1); + + string key = tok.value!; + + tok = _tokens[_tokenIndex++]; + if(tok.type != TokenType.Equals) + throw new UnexpectedTokenException(tok, _tokenIndex - 1); + + + object? value = ParseValue(); + if (value == null) + throw new UnexpectedTokenException(_tokens[_tokenIndex - 1], _tokenIndex - 1); + if(!dict.TryGetValue(key, out List? list)) + { + list = new List(); + dict.Add(key, list); + } + list.Add(value); + } + + return dict; + } + + public Dictionary> Parse() + { + Lex(); + if (_tokens.Count == 0) + throw new Exception("Save file is empty"); + + _tokenIndex = 0; + var root = ParseDict(); + return root; + } } \ No newline at end of file diff --git a/ParadoxSaveParser.Lib/ParserEU4.cs b/ParadoxSaveParser.Lib/ParserEU4.cs deleted file mode 100644 index b7ee253..0000000 --- a/ParadoxSaveParser.Lib/ParserEU4.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace ParadoxSaveParser.Lib; - -public class ParserEU4 : Parser -{ - public ParserEU4(Stream savefile) : base(savefile) - { - } - - public override SaveData Parse() - { - var saveData = new SaveData(); - - return saveData; - } -} \ No newline at end of file diff --git a/ParadoxSaveParser.Lib/SaveData.cs b/ParadoxSaveParser.Lib/SaveData.cs deleted file mode 100644 index dc352c8..0000000 --- a/ParadoxSaveParser.Lib/SaveData.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ParadoxSaveParser.Lib; - -public class SaveData -{ - -} \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/PathHelper.cs b/ParadoxSaveParser.WebAPI/PathHelper.cs index e853ca3..968b69a 100644 --- a/ParadoxSaveParser.WebAPI/PathHelper.cs +++ b/ParadoxSaveParser.WebAPI/PathHelper.cs @@ -7,6 +7,6 @@ public static class PathHelper public const string DATA_DIR = "data"; public static string SAVES_DIR = Path.Join(DATA_DIR, "saves"); public static string GetMetaFilePath(string save_id) => Path.Join(SAVES_DIR, save_id + ".meta.json"); - public static string GetEU4SaveFilePath(string save_id) => Path.Join(SAVES_DIR, save_id + ".eu4"); + public static string GetSaveFilePath(string save_id) => Path.Join(SAVES_DIR, save_id + ".eu4"); public static string GetParsedSaveFilePath(string save_id) => Path.Join(SAVES_DIR, save_id + ".parsed.json"); } \ No newline at end of file diff --git a/ParadoxSaveParser.WebAPI/Program.cs b/ParadoxSaveParser.WebAPI/Program.cs index a30e99f..dbf734f 100644 --- a/ParadoxSaveParser.WebAPI/Program.cs +++ b/ParadoxSaveParser.WebAPI/Program.cs @@ -5,7 +5,9 @@ global using System.Threading.Tasks; global using DTLib.Demystifier; global using ParadoxSaveParser.Lib; using System.Collections.Concurrent; +using System.IO.Compression; using System.Linq; +using System.Text; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -39,30 +41,12 @@ public class Program _app.UseHttpsRedirection(); _app.MapGet("/getSaveStatus", GetSaveStatusHandler); + _app.MapPost("/uploadSave/eu4", UploadSaveHandler); _app.MapPost("/parseSave/eu4", ParseSaveEU4Handler); _app.Run(); } - private static async Task GetSaveStatusHandler(HttpContext httpContext) - { - httpContext.Request.Query.TryGetValue("id", out var ids); - string? id = ids.FirstOrDefault(); - if (string.IsNullOrEmpty(id)) - { - throw new BadHttpRequestException("No id provided", - StatusCodes.Status400BadRequest); - } - - if (!_saveMetadataStorage.TryGetValue(id, out var meta)) - { - throw new BadHttpRequestException($"Save with {id} not found", - StatusCodes.Status400BadRequest); - } - - await httpContext.Response.WriteAsJsonAsync(meta); - } - - private static async Task ParseSaveEU4Handler(HttpContext httpContext) + private static async Task UploadSaveHandler(HttpContext httpContext) { var remoteFile = httpContext.Request.Form.Files.FirstOrDefault(); if (remoteFile is null || !remoteFile.FileName.EndsWith(".eu4")) @@ -84,22 +68,75 @@ public class Program { throw new BadHttpRequestException($"Guid collision! Can't create metadata with id {saveId}", StatusCodes.Status500InternalServerError); } + + meta.status = SaveFileProcessingStatus.Uploading; + string saveFilePath = PathHelper.GetSaveFilePath(meta.id); + await using var saveFile = File.Open(saveFilePath, FileMode.CreateNew, FileAccess.ReadWrite); + await using var remoteStream = remoteFile.OpenReadStream(); + await remoteStream.CopyToAsync(saveFile); + meta.status = SaveFileProcessingStatus.Uploaded; + + await httpContext.Response.WriteAsJsonAsync(meta); + } + + private static SaveFileMetadata GetMetaFromRequestId(HttpContext httpContext, string requestParamName) + { + httpContext.Request.Query.TryGetValue(requestParamName, out var ids); + string? id = ids.FirstOrDefault(); + if (string.IsNullOrEmpty(id)) + { + throw new BadHttpRequestException($"No request parameter '{requestParamName}' provided", + StatusCodes.Status400BadRequest); + } + + if (!_saveMetadataStorage.TryGetValue(id, out var meta)) + { + throw new BadHttpRequestException($"Save with {id} not found", + StatusCodes.Status400BadRequest); + } + + return meta; + } + + private static async Task GetSaveStatusHandler(HttpContext httpContext) + { + var meta = GetMetaFromRequestId(httpContext, "id"); + await httpContext.Response.WriteAsJsonAsync(meta); + } + + private static async Task ParseSaveEU4Handler(HttpContext httpContext) + { + var meta = GetMetaFromRequestId(httpContext, "id"); + if (meta.status == SaveFileProcessingStatus.Error) + { + httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; + await httpContext.Response.WriteAsJsonAsync(meta); + return; + } try { - meta.status = SaveFileProcessingStatus.Uploading; - string saveFilePath = PathHelper.GetEU4SaveFilePath(meta.id); - await using var saveFile = File.Open(saveFilePath, FileMode.CreateNew, FileAccess.ReadWrite); - await using (var remoteStream = remoteFile.OpenReadStream()) - { - await Task.Delay(50000); - await remoteStream.CopyToAsync(saveFile); - } - + if(meta.status != SaveFileProcessingStatus.Uploaded) + throw new Exception($"Invalid save processing status: {meta.status}"); + + using var zipArchive = ZipFile.Open(PathHelper.GetSaveFilePath(meta.id), ZipArchiveMode.Read); + var zipEntry = zipArchive.Entries.FirstOrDefault(e => e.Name == "gamestate"); + if(zipEntry is null) + throw new Exception("Invalid save format: no gamestate file found"); + string extractedGamestatePath = PathHelper.GetSaveFilePath(meta.id) + ".gamestate"; + zipEntry.ExtractToFile(extractedGamestatePath); + var gamestateStream = File.Open(extractedGamestatePath, FileMode.Open, FileAccess.Read); + meta.status = SaveFileProcessingStatus.Parsing; - saveFile.Seek(0, SeekOrigin.Begin); - var parser = new ParserEU4(saveFile); + string expectedHeader = "EU4txt"; + byte[] headBytes = new byte[expectedHeader.Length]; + gamestateStream.ReadExactly(headBytes); + string headStr = Encoding.UTF8.GetString(headBytes); + if(headStr != expectedHeader) + throw new Exception($"Invalid gamestate header: '{headStr}'"); + var parser = new Parser(gamestateStream); var result = parser.Parse(); + meta.status = SaveFileProcessingStatus.SavingResults; string resultFilePath = PathHelper.GetParsedSaveFilePath(meta.id); await using var resultFile = File.Open(resultFilePath, FileMode.CreateNew, FileAccess.Write); @@ -112,7 +149,8 @@ public class Program meta.status = SaveFileProcessingStatus.Error; string errorMesage = ex.ToStringDemystified(); meta.errorMesage = errorMesage; - _app.Logger.Log(LogLevel.Error, "EU4SaveParse Error: {errorMesage}", errorMesage); + httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; + _app.Logger.Log(LogLevel.Error, "ParseSaveEU4 Error: {errorMesage}", errorMesage); } await httpContext.Response.WriteAsJsonAsync(meta); diff --git a/ParadoxSaveParser.WebAPI/SaveFileMetadata.cs b/ParadoxSaveParser.WebAPI/SaveFileMetadata.cs index 13a534b..1bb3edc 100644 --- a/ParadoxSaveParser.WebAPI/SaveFileMetadata.cs +++ b/ParadoxSaveParser.WebAPI/SaveFileMetadata.cs @@ -6,7 +6,7 @@ namespace ParadoxSaveParser.WebAPI; public enum SaveFileProcessingStatus { - Initialized, Uploading, Parsing, SavingResults, Done, Error + Initialized, Uploading, Uploaded, Parsing, SavingResults, Done, Error } public enum Game @@ -23,9 +23,7 @@ public class SaveFileMetadata [JsonConverter(typeof(JsonStringEnumConverter))] public required SaveFileProcessingStatus status { get; set; } - - [JsonIgnore] public string? errorMesage { get; set; } private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true };