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