using System; using System.Collections.Generic; using System.IO; using System.Linq; using Aspose.Gis; using GitConverter.Lib.Licensing; using GitConverter.Lib.Logging; using GitConverter.Lib.Models; using SharpCompress.Archives; namespace GitConverter.Lib.Converters { /// /// Converter that handles Shapefile inputs and produces an output according to the requested target format. /// /// /// Responsibilities /// - Validate input / output / temp paths (delegates to ). /// - Accept an archive containing shapefile components (.shp, .shx, .dbf) — the archive is extracted into the provided temp folder. /// - Extraction is performed safely with zip-slip protection and per-entry error handling. /// - After obtaining a usable .shp path the converter resolves the destination Aspose driver via /// and invokes Aspose.GIS . /// - Applies embedded Aspose license via before conversion. /// public class ShapefileConverter : IConverter { private static readonly string[] RequiredShapefileExtensions = new[] { ".shp", ".shx", ".dbf" }; /// /// Convert a shapefile archive into the requested target format. /// public ConversionResult Convert(string gisInputFilePath, string gisTargetFormatOption, string outputFolderPath, string tempFolderPath) { Log.Debug($"ShapefileConverter.Convert params: gisInputFilePath='{gisInputFilePath}', gisTargetFormatOption='{gisTargetFormatOption}', outputFolderPath='{outputFolderPath}', tempFolderPath='{tempFolderPath}'"); Log.Info($"ShapefileConverter: starting conversion (option='{gisTargetFormatOption}', input='{gisInputFilePath}')."); var tempFolderNotExistedAtStart = !string.IsNullOrWhiteSpace(tempFolderPath) && !Directory.Exists(tempFolderPath); var validation = ConverterUtils.ValidateAndPreparePaths(gisInputFilePath, outputFolderPath, tempFolderPath); if (validation != null) return validation; string sourceShpPath = gisInputFilePath; var extractedToTemp = false; try { var inputIsArchive = ConverterUtils.IsArchiveFile(gisInputFilePath); if (!inputIsArchive) { Log.Error($"ShapefileConverter: input '{gisInputFilePath}' is not an archive. Shapefile conversions require an archive containing .shp/.shx/.dbf."); return ConversionResult.Failure("Shapefile inputs must be provided as an archive containing .shp, .shx and .dbf."); } Log.Debug("ShapefileConverter: input detected as archive. Inspecting entries."); var entries = ConverterUtils.TryListArchiveEntries(gisInputFilePath); if (entries == null) { Log.Error("ShapefileConverter: failed to list archive entries."); return ConversionResult.Failure("Failed to inspect archive contents."); } // Quick check that the archive contains the required extensions (by file names / segments) var entryExts = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var entry in entries) { try { var ext = Path.GetExtension(entry); if (!string.IsNullOrEmpty(ext)) entryExts.Add(ext.ToLowerInvariant()); var segments = entry.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); foreach (var seg in segments) { var idx = seg.LastIndexOf('.'); if (idx > 0 && idx < seg.Length - 1) entryExts.Add(seg.Substring(idx).ToLowerInvariant()); } } catch { // ignore malformed entry names } } var missingRequired = RequiredShapefileExtensions.Where(req => !entryExts.Contains(req)).ToArray(); if (missingRequired.Length > 0) { Log.Error($"ShapefileConverter: archive missing required components: {string.Join(", ", missingRequired)}"); return ConversionResult.Failure($"Archive missing required shapefile components: {string.Join(", ", missingRequired)}"); } // Extract safely into tempFolderPath try { Log.Debug($"ShapefileConverter: 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) { if (entry == null || entry.IsDirectory) continue; 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($"ShapefileConverter: skipping entry '{entryKey}' that would extract outside 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($"ShapefileConverter: extracted '{entryKey}' -> '{destFull}'."); } catch (Exception exEntry) { Log.Error($"ShapefileConverter: failed to extract '{entryKey}': {exEntry.Message}"); skipped.Add(entryKey); } } } if (skipped.Count > 0) { Log.Error($"ShapefileConverter: 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)}"); } var extractedExts = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var f in Directory.GetFiles(tempFolderPath, "*.*", SearchOption.AllDirectories)) extractedExts.Add(Path.GetExtension(f)); var stillMissing = RequiredShapefileExtensions.Where(r => !extractedExts.Contains(r)).ToArray(); if (stillMissing.Length > 0) { Log.Error($"ShapefileConverter: after extraction missing components: {string.Join(", ", stillMissing)}"); try { ConverterUtils.CleanupExtractedFiles(Directory.GetFiles(tempFolderPath, "*.*", SearchOption.AllDirectories)); } catch { } return ConversionResult.Failure($"Archive did not contain required shapefile components after extraction: {string.Join(", ", stillMissing)}"); } var shpFiles = Directory.GetFiles(tempFolderPath, "*.shp", SearchOption.AllDirectories); if (shpFiles.Length == 0) { Log.Error("ShapefileConverter: no .shp file found after extraction."); try { ConverterUtils.CleanupExtractedFiles(Directory.GetFiles(tempFolderPath, "*.*", SearchOption.AllDirectories)); } catch { } return ConversionResult.Failure("Archive did not contain a usable .shp file after extraction."); } // pick first .shp sourceShpPath = shpFiles[0]; extractedToTemp = true; Log.Info($"ShapefileConverter: using extracted .shp '{sourceShpPath}'."); } catch (Exception ex) { Log.Error($"ShapefileConverter: extraction failed: {ex.Message}", ex); try { ConverterUtils.CleanupExtractedFiles(Directory.GetFiles(tempFolderPath, "*.*", SearchOption.AllDirectories)); } catch { } return ConversionResult.Failure($"Failed to extract shapefile archive: {ex.Message}"); } // Resolve destination driver and run Aspose conversion var destDriverGeneric = ConverterUtils.ConversionOptionToDriver(gisTargetFormatOption); if (destDriverGeneric == null) { Log.Warn($"ShapefileConverter: target option '{gisTargetFormatOption}' did not map to a known driver."); return ConversionResult.Failure($"Target format option '{gisTargetFormatOption}' did not map to a known Aspose driver."); } var destFileDriver = destDriverGeneric as FileDriver; var srcFileDriver = Drivers.Shapefile as FileDriver; Log.Info($"ShapefileConverter: preparing Aspose conversion source='{sourceShpPath}', target='{gisTargetFormatOption}'."); if (srcFileDriver == null) { Log.Error("ShapefileConverter: Aspose Drivers.Shapefile is not available as a FileDriver."); return ConversionResult.Failure("Internal error: source driver not available."); } if (destFileDriver == null) { Log.Error($"ShapefileConverter: resolved destination driver is not a FileDriver for option '{gisTargetFormatOption}'."); return ConversionResult.Failure("Internal error: destination driver not available."); } var licenseApplied = AsposeLicenseManager.ApplyLicense(); if (!licenseApplied) { Log.Error("ShapefileConverter: Aspose license 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("ShapefileConverter: 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 Shapefile. var maybeOutFileExt = FileExtensionHelpers.FromOption(gisTargetFormatOption); var outFileExt = maybeOutFileExt ?? FileExtension.Shapefile; if (maybeOutFileExt == null) Log.Warn($"ShapefileConverter: could not map option '{gisTargetFormatOption}' to a known FileExtension; falling back to '{outFileExt}'."); var extDot = FileExtensionHelpers.ToDotExtension(outFileExt); var destOutputPath = Path.Combine(outputFolderPath, $"output_{timeStamp}{extDot}"); Log.Info($"ShapefileConverter: target output file will be '{destOutputPath}'."); Log.Info($"ShapefileConverter: converting '{sourceShpPath}' -> '{destOutputPath}' (target option='{gisTargetFormatOption}')."); // Perform conversion via Aspose VectorLayer.Convert(sourceShpPath, srcFileDriver, destOutputPath, destFileDriver); Log.Info("ShapefileConverter: Aspose conversion succeeded."); return ConversionResult.Success($"Converted with Aspose to {gisTargetFormatOption}; output: {destOutputPath}"); } catch (Exception ex) { Log.Error($"ShapefileConverter: unexpected error: {ex.Message}", ex); return ConversionResult.Failure($"Unexpected error: {ex.Message}"); } finally { Log.Info("ShapefileConverter: finished conversion attempt."); if (extractedToTemp || tempFolderNotExistedAtStart) { Log.Debug("ShapefileConverter: cleaning up temp folder."); try { ConverterUtils.TryCleanupTempFolder(tempFolderPath); } catch (Exception ex) { Log.Debug($"ShapefileConverter: cleanup failed: {ex.Message}"); } } } } } }