using System; using System.Collections.Generic; using System.IO; using System.Linq; using GitConverter.Lib.Licensing; using GitConverter.Lib.Logging; using GitConverter.Lib.Models; using SharpCompress.Archives; using Aspose.Gis; namespace GitConverter.Lib.Converters { /// /// Converter that handles CSV inputs and produces outputs according to the requested target format. /// /// /// Responsibilities /// - Validate input / output / temp paths (delegates to ). /// - Accept either: /// * a single .csv file as input, or /// * an archive containing one or more .csv entries (the first .csv entry found is used). /// - When given an archive, extract entries into the provided temp folder using safe extraction (zip-slip protection). /// - Map the requested target option to an Aspose driver via . /// - Apply Aspose license via and call Aspose.GIS vector conversion APIs. /// public class CsvConverter : IConverter { /// /// Convert the given CSV input (single .csv or an archive containing .csv entries) into the requested target format. /// public ConversionResult Convert(string gisInputFilePath, string gisTargetFormatOption, string outputFolderPath, string tempFolderPath) { // Parameter trace for diagnostics Log.Debug($"CsvConverter.Convert params: gisInputFilePath='{gisInputFilePath}', gisTargetFormatOption='{gisTargetFormatOption}', outputFolderPath='{outputFolderPath}', tempFolderPath='{tempFolderPath}'"); Log.Info($"CsvConverter: starting conversion (option='{gisTargetFormatOption}', input='{gisInputFilePath}')."); // Remember whether the temp folder existed before this call. var tempFolderNotExistedAtStart = !string.IsNullOrWhiteSpace(tempFolderPath) && !Directory.Exists(tempFolderPath); // Validate and prepare paths var validation = ConverterUtils.ValidateAndPreparePaths(gisInputFilePath, outputFolderPath, tempFolderPath); if (validation != null) return validation; string sourceCsvPath = gisInputFilePath; var extractedToTemp = false; try { var inputIsArchive = ConverterUtils.IsArchiveFile(gisInputFilePath); if (inputIsArchive) { Log.Debug("CsvConverter: input detected as archive. Inspecting entries."); var entries = ConverterUtils.TryListArchiveEntries(gisInputFilePath); if (entries == null) { Log.Error("CsvConverter: failed to list archive entries."); return ConversionResult.Failure("Failed to inspect archive contents."); } // Find first .csv entry (case-insensitive) var csvEntry = entries.FirstOrDefault(e => string.Equals(Path.GetExtension(e), ".csv", StringComparison.OrdinalIgnoreCase)); if (string.IsNullOrWhiteSpace(csvEntry)) { Log.Error("CsvConverter: archive does not contain a .csv entry."); return ConversionResult.Failure("Archive does not contain a .csv entry."); } // Extract archive safely into tempFolderPath try { Log.Debug($"CsvConverter: extracting archive '{gisInputFilePath}' into '{tempFolderPath}'."); if (!Directory.Exists(tempFolderPath)) { Directory.CreateDirectory(tempFolderPath); } var tempFull = Path.GetFullPath(tempFolderPath).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; var skipped = new List(); var extractedFiles = new List(); using (var archive = ArchiveFactory.Open(gisInputFilePath)) { foreach (var entry in archive.Entries.Where(en => !en.IsDirectory)) { var entryKey = entry.Key; if (string.IsNullOrEmpty(entryKey)) continue; var destPath = Path.Combine(tempFolderPath, entryKey); var destFull = Path.GetFullPath(destPath); // zip-slip guard if (!destFull.StartsWith(tempFull, StringComparison.OrdinalIgnoreCase)) { Log.Warn($"CsvConverter: skipping entry '{entryKey}' which would extract outside the temp folder."); skipped.Add(entryKey); continue; } try { Directory.CreateDirectory(Path.GetDirectoryName(destFull) ?? tempFolderPath); using (var src = entry.OpenEntryStream()) using (var dst = File.Create(destFull)) { src.CopyTo(dst); } extractedFiles.Add(destFull); Log.Debug($"CsvConverter: extracted '{entryKey}' -> '{destFull}'."); } catch (Exception exEntry) { Log.Error($"CsvConverter: failed to extract '{entryKey}': {exEntry.Message}"); skipped.Add(entryKey); } } } // If any entries were skipped or failed to extract -> treat as extraction failure if (skipped.Count > 0) { Log.Error($"CsvConverter: some archive entries skipped or failed: {string.Join(", ", skipped)}"); try { ConverterUtils.CleanupExtractedFiles(Directory.GetFiles(tempFolderPath, "*.*", SearchOption.AllDirectories)); } catch { } return ConversionResult.Failure($"Archive extraction failed for entries: {string.Join(", ", skipped)}"); } // locate extracted .csv var found = Directory.GetFiles(tempFolderPath, "*.csv", SearchOption.AllDirectories).FirstOrDefault(); if (found == null) { Log.Error("CsvConverter: extracted archive but no .csv file found."); try { ConverterUtils.CleanupExtractedFiles(Directory.GetFiles(tempFolderPath, "*.*", SearchOption.AllDirectories)); } catch { } return ConversionResult.Failure("Archive extraction did not yield a .csv file."); } sourceCsvPath = found; extractedToTemp = true; Log.Info($"CsvConverter: using extracted CSV '{sourceCsvPath}'."); } catch (Exception ex) { Log.Error($"CsvConverter: extraction failed: {ex.Message}", ex); try { ConverterUtils.CleanupExtractedFiles(Directory.GetFiles(tempFolderPath, "*.*", SearchOption.AllDirectories)); } catch { } return ConversionResult.Failure($"Failed to extract CSV from archive: {ex.Message}"); } } else { // Single-file path must be .csv and exist var ext = Path.GetExtension(gisInputFilePath) ?? string.Empty; if (!string.Equals(ext, ".csv", StringComparison.OrdinalIgnoreCase)) { Log.Error($"CsvConverter: input '{gisInputFilePath}' is not a .csv file."); return ConversionResult.Failure("CSV converter requires a .csv file or archive containing .csv."); } if (!File.Exists(gisInputFilePath)) { Log.Error($"CsvConverter: input file does not exist: '{gisInputFilePath}'."); return ConversionResult.Failure($"Input file does not exist: '{gisInputFilePath}'."); } sourceCsvPath = gisInputFilePath; Log.Debug($"CsvConverter: using input CSV '{sourceCsvPath}'."); } // Resolve destination driver via ConverterUtils var destDriverGeneric = ConverterUtils.ConversionOptionToDriver(gisTargetFormatOption); if (destDriverGeneric == null) { Log.Warn($"CsvConverter: target format option '{gisTargetFormatOption}' did not map to a known Aspose driver."); return ConversionResult.Failure($"Target format option '{gisTargetFormatOption}' did not map to a known Aspose driver."); } var destFileDriver = destDriverGeneric as FileDriver; var srcFileDriver = Drivers.Csv as FileDriver; Log.Info($"CsvConverter: preparing Aspose conversion source='{sourceCsvPath}', target='{gisTargetFormatOption}'."); if (srcFileDriver == null) { Log.Error("CsvConverter: Aspose Drivers.Csv is not available as a FileDriver."); return ConversionResult.Failure("Internal error: source driver not available."); } if (destFileDriver == null) { Log.Error($"CsvConverter: resolved destination driver for '{gisTargetFormatOption}' is not a FileDriver."); return ConversionResult.Failure("Internal error: destination driver not available."); } // Apply Aspose license (embedded) var licenseApplied = AsposeLicenseManager.ApplyLicense(); if (!licenseApplied) { Log.Error("CsvConverter: Aspose license was not applied (embedded). Aborting conversion."); return ConversionResult.Failure("Aspose license not applied (embedded). Place Aspose.Total.Net.lic as an EmbeddedResource in GitConverter.Lib for releases."); } Log.Info("CsvConverter: Aspose license applied successfully (embedded)."); // Ensure output folder exists and prepare deterministic output filename Directory.CreateDirectory(outputFolderPath); var timeStamp = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss"); // Determine output extension based on target option. FromOption may return null; fall back to Csv. var maybeOutFileExt = FileExtensionHelpers.FromOption(gisTargetFormatOption); var outFileExt = maybeOutFileExt ?? FileExtension.Csv; if (maybeOutFileExt == null) Log.Warn($"CsvConverter: could not map option '{gisTargetFormatOption}' to a known FileExtension; falling back to '{outFileExt}'."); var extDot = FileExtensionHelpers.ToDotExtension(outFileExt); var destCsvOutputPath = Path.Combine(outputFolderPath, $"output_{timeStamp}{extDot}"); Log.Info($"CsvConverter: target output file will be '{destCsvOutputPath}'."); Log.Info($"CsvConverter: converting '{sourceCsvPath}' -> '{destCsvOutputPath}' (target option='{gisTargetFormatOption}')."); // Perform conversion via Aspose VectorLayer.Convert(sourceCsvPath, srcFileDriver, destCsvOutputPath, destFileDriver); Log.Info("CsvConverter: Aspose conversion succeeded."); return ConversionResult.Success($"Converted with Aspose to {gisTargetFormatOption}; output: {destCsvOutputPath}"); } catch (Exception ex) { Log.Error($"CsvConverter: unexpected error: {ex.Message}", ex); return ConversionResult.Failure($"Unexpected error: {ex.Message}"); } finally { Log.Info("CsvConverter: finished conversion attempt."); // Only cleanup when we actually extracted files into the temp folder if (extractedToTemp || tempFolderNotExistedAtStart) { Log.Debug("CsvConverter: cleaning up temp folder."); try { ConverterUtils.TryCleanupTempFolder(tempFolderPath); } catch (Exception ex) { Log.Debug($"CsvConverter: cleanup failed: {ex.Message}"); } } Log.Info("CsvConverter: finished conversion attempt."); } } } }