using System.IO.Compression; using LiquidCode.Tester.Common.Models; namespace LiquidCode.Tester.Worker.Services; public class PackageParserService : IPackageParserService { private readonly ILogger _logger; private readonly PolygonProblemXmlParser _polygonParser; private readonly AnswerGenerationService _answerGenerator; private readonly CppCompilationService _cppCompilation; public PackageParserService( ILogger logger, PolygonProblemXmlParser polygonParser, AnswerGenerationService answerGenerator, CppCompilationService cppCompilation) { _logger = logger; _polygonParser = polygonParser; _answerGenerator = answerGenerator; _cppCompilation = cppCompilation; } public async Task ParsePackageAsync(Stream packageStream) { var workingDirectory = Path.Combine(Path.GetTempPath(), $"problem_{Guid.NewGuid()}"); Directory.CreateDirectory(workingDirectory); _logger.LogInformation("Extracting package to {WorkingDirectory}", workingDirectory); try { // Extract ZIP archive using var archive = new ZipArchive(packageStream, ZipArchiveMode.Read); archive.ExtractToDirectory(workingDirectory); // Check if this is a Polygon package (has problem.xml) var problemXmlPath = Path.Combine(workingDirectory, "problem.xml"); if (File.Exists(problemXmlPath)) { _logger.LogInformation("Detected Polygon package format (problem.xml found)"); return await ParsePolygonPackageAsync(workingDirectory, problemXmlPath); } // Fall back to legacy format (.in/.out files) _logger.LogInformation("Using legacy package format (.in/.out files)"); return await ParseLegacyPackage(workingDirectory); } catch (Exception ex) { _logger.LogError(ex, "Failed to parse package"); throw; } } private async Task ParsePolygonPackageAsync(string workingDirectory, string problemXmlPath) { var descriptor = _polygonParser.ParseProblemXml(problemXmlPath); if (descriptor == null) { _logger.LogWarning("Failed to parse problem.xml, falling back to legacy format"); return await ParseLegacyPackage(workingDirectory); } var package = new ProblemPackage { WorkingDirectory = workingDirectory, DefaultTimeLimit = descriptor.TimeLimitMs, DefaultMemoryLimit = descriptor.MemoryLimitMb }; // Collect test file paths and check which answers are missing var inputPaths = new List(); var answerPaths = new List(); var missingAnswerPaths = new List(); var missingAnswerInputs = new List(); for (int i = 1; i <= descriptor.TestCount; i++) { var inputPath = Path.Combine(workingDirectory, string.Format(descriptor.InputPathPattern.Replace("%02d", "{0:D2}"), i)); var answerPath = Path.Combine(workingDirectory, string.Format(descriptor.AnswerPathPattern.Replace("%02d", "{0:D2}"), i)); if (!File.Exists(inputPath)) { _logger.LogWarning("Input file not found: {InputPath}", inputPath); continue; } inputPaths.Add(inputPath); answerPaths.Add(answerPath); if (!File.Exists(answerPath)) { missingAnswerPaths.Add(answerPath); missingAnswerInputs.Add(inputPath); } } // Generate missing answer files if we have a main solution if (missingAnswerPaths.Count > 0) { _logger.LogInformation("Found {Count} tests without answer files, attempting to generate them", missingAnswerPaths.Count); var generated = await _answerGenerator.GenerateAnswersAsync( descriptor, workingDirectory, missingAnswerInputs, missingAnswerPaths); if (generated) { _logger.LogInformation("Successfully generated answer files"); } else { _logger.LogWarning("Failed to generate answer files, tests without answers will be skipped"); } } // Now create test cases for all tests that have answer files for (int i = 0; i < inputPaths.Count; i++) { var inputPath = inputPaths[i]; var answerPath = answerPaths[i]; if (!File.Exists(answerPath)) { _logger.LogWarning("Answer file not found: {AnswerPath} (skipping test)", answerPath); continue; } package.TestCases.Add(new TestCase { Number = i + 1, InputFilePath = inputPath, OutputFilePath = answerPath, TimeLimit = descriptor.TimeLimitMs, MemoryLimit = descriptor.MemoryLimitMb }); } // Look for and compile checker package.CheckerPath = await FindAndCompileCheckerAsync(workingDirectory); if (package.TestCases.Count == 0) { _logger.LogWarning("No test cases with answer files found! Expected format: {InputPattern} -> {AnswerPattern}", descriptor.InputPathPattern, descriptor.AnswerPathPattern); } _logger.LogInformation("Parsed Polygon package with {TestCount} tests (out of {TotalTests} in problem.xml)", package.TestCases.Count, descriptor.TestCount); return package; } private async Task ParseLegacyPackage(string workingDirectory) { var package = new ProblemPackage { WorkingDirectory = workingDirectory }; // Find tests directory var testsDir = Path.Combine(workingDirectory, "tests"); if (!Directory.Exists(testsDir)) { _logger.LogWarning("Tests directory not found, searching for test files in root"); testsDir = workingDirectory; } // Parse test cases var inputFiles = Directory.GetFiles(testsDir, "*", SearchOption.AllDirectories) .Where(f => Path.GetFileName(f).EndsWith(".in") || Path.GetFileName(f).Contains("input")) .OrderBy(f => f) .ToList(); for (int i = 0; i < inputFiles.Count; i++) { var inputFile = inputFiles[i]; var outputFile = FindCorrespondingOutputFile(inputFile); if (outputFile == null) { _logger.LogWarning("No output file found for input {InputFile}", inputFile); continue; } package.TestCases.Add(new TestCase { Number = i + 1, InputFilePath = inputFile, OutputFilePath = outputFile, TimeLimit = package.DefaultTimeLimit, MemoryLimit = package.DefaultMemoryLimit }); } // Look for and compile checker package.CheckerPath = await FindAndCompileCheckerAsync(workingDirectory); if (package.TestCases.Count == 0) { _logger.LogWarning("No test cases found! Check package structure. Expected .in/.out files in tests directory or root"); } _logger.LogInformation("Parsed legacy package with {TestCount} tests", package.TestCases.Count); return package; } private string? FindCorrespondingOutputFile(string inputFile) { var directory = Path.GetDirectoryName(inputFile)!; var fileName = Path.GetFileNameWithoutExtension(inputFile); var extension = Path.GetExtension(inputFile); // Try various output file naming patterns var patterns = new[] { fileName.Replace("input", "output") + ".out", fileName.Replace("input", "output") + ".a", fileName.Replace("input", "answer") + ".out", fileName.Replace("input", "answer") + ".a", fileName + ".out", fileName + ".a", fileName.Replace(".in", ".out"), fileName.Replace(".in", ".a") }; foreach (var pattern in patterns) { var candidate = Path.Combine(directory, pattern); if (File.Exists(candidate)) { return candidate; } } return null; } private async Task FindAndCompileCheckerAsync(string workingDirectory) { // Try to find checker in common locations var checkerCandidates = new[] { Path.Combine(workingDirectory, "check.exe"), Path.Combine(workingDirectory, "checker.exe"), Path.Combine(workingDirectory, "check.cpp"), Path.Combine(workingDirectory, "checker.cpp"), Path.Combine(workingDirectory, "files", "check.exe"), Path.Combine(workingDirectory, "files", "checker.exe"), Path.Combine(workingDirectory, "files", "check.cpp"), Path.Combine(workingDirectory, "files", "checker.cpp") }; foreach (var candidate in checkerCandidates) { if (!File.Exists(candidate)) continue; // If it's already an executable, use it if (candidate.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) { _logger.LogInformation("Found checker executable: {CheckerPath}", candidate); return candidate; } // If it's C++ source, compile it if (candidate.EndsWith(".cpp", StringComparison.OrdinalIgnoreCase)) { _logger.LogInformation("Found checker source: {CheckerPath}, compiling...", candidate); try { var checkerSource = await File.ReadAllTextAsync(candidate); var checkerDir = Path.GetDirectoryName(candidate)!; // Compile checker with C++17 (testlib.h compatible) var compilationResult = await _cppCompilation.CompileAsync( checkerSource, checkerDir, "17"); if (!compilationResult.Success) { _logger.LogError("Failed to compile checker: {Error}", compilationResult.CompilerOutput); continue; // Try next candidate } _logger.LogInformation("Checker compiled successfully: {ExecutablePath}", compilationResult.ExecutablePath); return compilationResult.ExecutablePath; } catch (Exception ex) { _logger.LogError(ex, "Error compiling checker: {CheckerPath}", candidate); continue; // Try next candidate } } } _logger.LogWarning("No checker found in package"); return null; } }