using System.Text;
using System.Text.RegularExpressions;
using DbfDataReader;
using DataConnection.Interfaces;
namespace DataConnection.DB.FoxPro;
///
/// Helper di basso livello per leggere database Visual FoxPro / dBase in modo
/// completamente managed (nessun provider OLE DB/ODBC richiesto, funziona a 64-bit).
///
/// Legge direttamente i file .dbf (tabelle), .fpt (campi memo) e, quando
/// la sorgente è un container .dbc, ne legge il catalogo per ricavare l'elenco
/// ufficiale delle tabelle. La libreria sottostante è DbfDataReader (MIT).
///
/// Sola lettura: il coupler usa FoxPro come sorgente dati.
///
public static class FoxProReader
{
static FoxProReader()
{
// Le code page non-Unicode (es. 1252, 1250, 850) non sono disponibili di default
// su .NET Core/5+: serve registrare il provider.
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
}
/// Code page di default (Windows-1252, ANSI Europa occidentale).
public const int DefaultCodePage = 1252;
// Mappa byte "language driver" dell'header DBF -> code page più comuni.
private static readonly Dictionary LanguageDriverCodePages = new()
{
[0x01] = 437, // U.S. MS-DOS
[0x02] = 850, // International MS-DOS
[0x03] = 1252, // Windows ANSI (più comune nei dati VFP)
[0x08] = 865, // Danish OEM
[0x09] = 437, // Dutch OEM
[0x0A] = 850, // Dutch OEM*
[0x64] = 852, // Eastern European MS-DOS
[0x65] = 866, // Russian MS-DOS
[0x66] = 865, // Nordic MS-DOS
[0x67] = 861, // Icelandic MS-DOS
[0x6A] = 737, // Greek MS-DOS
[0x6B] = 857, // Turkish MS-DOS
[0x57] = 1252, // ANSI
[0xC8] = 1250, // Eastern European Windows
[0xC9] = 1251, // Russian Windows
[0xCA] = 1254, // Turkish Windows
[0xCB] = 1253, // Greek Windows
};
///
/// Interpreta la "connection string" FoxPro (un percorso, eventualmente con opzioni)
/// e risolve cartella, eventuale .dbc, encoding e gestione record cancellati.
///
/// Formati accettati:
///
/// - percorso nudo a un file .dbc: C:\dati\Data.dbc
/// - percorso nudo a una cartella di .dbf: C:\dati
/// - stringa con chiavi: Data Source=C:\dati\Data.dbc;CodePage=1252;IncludeDeleted=false
/// - stringa stile OLE DB VFP: Provider=vfpoledb;Data Source=C:\dati\Data.dbc;... (Data Source viene estratto)
///
///
public static FoxProConnectionInfo Resolve(string connectionString)
{
if (string.IsNullOrWhiteSpace(connectionString))
throw new ArgumentException("Percorso del database FoxPro non specificato.", nameof(connectionString));
string? path = null;
int? codePage = null;
bool includeDeleted = false;
// Se contiene almeno una coppia chiave=valore, interpretiamo come connection string;
// altrimenti l'intera stringa è il percorso.
if (connectionString.Contains('='))
{
foreach (var segment in connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
var idx = segment.IndexOf('=');
if (idx < 0)
{
// Segmento senza '=': potrebbe essere un percorso nudo mescolato — usalo se non abbiamo già un path
path ??= segment.Trim();
continue;
}
var key = segment[..idx].Trim();
var value = segment[(idx + 1)..].Trim().Trim('"', '\'');
switch (key.Replace(" ", "").ToLowerInvariant())
{
case "datasource":
case "source":
case "data":
path = value;
break;
case "codepage":
case "encoding":
if (int.TryParse(value, out var cp))
codePage = cp;
else
codePage = TryGetCodePageByName(value);
break;
case "includedeleted":
case "deleted":
includeDeleted = value.Equals("true", StringComparison.OrdinalIgnoreCase)
|| value.Equals("on", StringComparison.OrdinalIgnoreCase)
|| value == "1";
break;
// "Provider", "Collating Sequence", ecc. sono ignorati (compatibilità OLE DB)
}
}
}
else
{
path = connectionString.Trim();
}
path = path?.Trim().Trim('"', '\'');
if (string.IsNullOrWhiteSpace(path))
throw new ArgumentException("Impossibile determinare il percorso del database FoxPro dalla stringa fornita.", nameof(connectionString));
string folder;
string? dbcPath = null;
if (path.EndsWith(".dbc", StringComparison.OrdinalIgnoreCase))
{
dbcPath = path;
folder = Path.GetDirectoryName(path) ?? path;
}
else
{
folder = path.TrimEnd('\\', '/');
// Se la cartella contiene un singolo .dbc e nessuna ambiguità, lo usiamo come container.
if (Directory.Exists(folder))
{
var dbcs = Directory.GetFiles(folder, "*.dbc");
if (dbcs.Length == 1)
dbcPath = dbcs[0];
}
}
var encoding = ResolveEncoding(codePage, dbcPath, folder);
return new FoxProConnectionInfo
{
Folder = folder,
DbcPath = dbcPath,
Encoding = encoding,
IncludeDeleted = includeDeleted
};
}
/// Verifica che la sorgente sia raggiungibile (cartella esistente, eventuale .dbc presente).
public static void EnsureReachable(FoxProConnectionInfo info)
{
if (info.IsContainer && !File.Exists(info.DbcPath!))
throw new FileNotFoundException($"File database container (.dbc) non trovato: {info.DbcPath}");
if (!Directory.Exists(info.Folder))
throw new DirectoryNotFoundException($"Cartella del database FoxPro non trovata: {info.Folder}");
}
///
/// Elenco delle tabelle: dal catalogo del .dbc (OBJECTTYPE='Table') se container,
/// altrimenti enumerando i file .dbf nella cartella. Vengono restituite solo le
/// tabelle effettivamente apribili (file .dbf presente).
///
public static List GetTableNames(FoxProConnectionInfo info)
{
EnsureReachable(info);
var names = new List();
var seen = new HashSet(StringComparer.OrdinalIgnoreCase);
if (info.IsContainer)
{
foreach (var name in ReadTableNamesFromContainer(info))
{
if (seen.Add(name) && File.Exists(ResolveTableFilePath(info.Folder, name)))
names.Add(name);
}
}
else
{
foreach (var file in Directory.EnumerateFiles(info.Folder, "*.dbf", SearchOption.TopDirectoryOnly))
{
var name = Path.GetFileNameWithoutExtension(file);
if (seen.Add(name))
names.Add(name);
}
}
names.Sort(StringComparer.OrdinalIgnoreCase);
return names;
}
/// Schema (colonne) di una singola tabella.
public static List GetTableColumns(FoxProConnectionInfo info, string tableName)
{
var filePath = ResolveTableFilePath(info.Folder, tableName);
if (!File.Exists(filePath))
throw new FileNotFoundException($"Tabella FoxPro non trovata: {filePath}");
using var table = new DbfTable(filePath, info.Encoding);
var columns = new List();
foreach (var col in table.Columns)
{
columns.Add(new DbColumnInfo
{
Name = col.ColumnName.Trim(),
DataType = MapColumnType(col),
IsNullable = true, // VFP: nullability non esposta in modo affidabile dall'header
IsPrimaryKey = false // le tabelle .dbf non hanno una PK affidabile; la chiave si sceglie nel coupler
});
}
return columns;
}
///
/// Legge i record di una tabella come dizionari nome-colonna -> valore.
/// I valori null sono rappresentati con (coerente con gli altri manager).
/// Streaming: non carica l'intera tabella in memoria.
///
public static IEnumerable> ReadRecords(
FoxProConnectionInfo info, string tableName, int? maxRows = null)
{
var filePath = ResolveTableFilePath(info.Folder, tableName);
if (!File.Exists(filePath))
throw new FileNotFoundException($"Tabella FoxPro non trovata: {filePath}");
var options = new DbfDataReaderOptions
{
Encoding = info.Encoding,
SkipDeletedRecords = !info.IncludeDeleted
};
using var reader = new DbfDataReader.DbfDataReader(filePath, options);
// Nomi colonna (trimmati) per ordinale.
var names = reader.DbfTable.Columns.Select(c => c.ColumnName.Trim()).ToArray();
long emitted = 0;
while (reader.Read())
{
var row = new Dictionary(names.Length, StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < names.Length; i++)
{
row[names[i]] = reader.IsDBNull(i) ? DBNull.Value : reader.GetValue(i);
}
yield return row;
emitted++;
if (maxRows.HasValue && emitted >= maxRows.Value)
yield break;
}
}
/// Numero di record (header) di una tabella, senza scorrere i dati.
public static long GetRecordCount(FoxProConnectionInfo info, string tableName)
{
var filePath = ResolveTableFilePath(info.Folder, tableName);
if (!File.Exists(filePath))
return 0;
using var table = new DbfTable(filePath, info.Encoding);
return table.Header.RecordCount;
}
///
/// Estrae il nome tabella (e un eventuale TOP n) da una semplice query SELECT.
/// Gestisce anche le query "wrappate" generate da CreateLimitedQuery
/// (SELECT TOP n * FROM (SELECT ... FROM tabella) AS subquery): viene preso
/// l'ultimo identificatore dopo FROM, cioè la tabella reale più interna.
///
public static (string Table, int? Top)? ParseSimpleSelect(string sql)
{
if (string.IsNullOrWhiteSpace(sql))
return null;
var fromMatches = Regex.Matches(sql, @"\bFROM\s+\[?([A-Za-z_][\w]*)\]?", RegexOptions.IgnoreCase);
if (fromMatches.Count == 0)
return null;
var table = fromMatches[^1].Groups[1].Value;
int? top = null;
var topMatch = Regex.Match(sql, @"\bTOP\s+(\d+)", RegexOptions.IgnoreCase);
if (topMatch.Success && int.TryParse(topMatch.Groups[1].Value, out var t))
top = t;
return (table, top);
}
/// Risolve il path del file .dbf per una tabella, gestendo il case-insensitive di Windows.
public static string ResolveTableFilePath(string folder, string tableName)
{
var name = tableName.Trim().Trim('[', ']', '"', '\'');
if (name.EndsWith(".dbf", StringComparison.OrdinalIgnoreCase))
name = name[..^4];
var direct = Path.Combine(folder, name + ".dbf");
if (File.Exists(direct))
return direct;
// Ricerca case-insensitive (i cataloghi .dbc spesso usano un case diverso dai file).
if (Directory.Exists(folder))
{
foreach (var file in Directory.EnumerateFiles(folder, "*.dbf", SearchOption.TopDirectoryOnly))
{
if (string.Equals(Path.GetFileNameWithoutExtension(file), name, StringComparison.OrdinalIgnoreCase))
return file;
}
}
return direct; // non trovato: il chiamante gestirà il FileNotFound
}
private static IEnumerable ReadTableNamesFromContainer(FoxProConnectionInfo info)
{
// Il file .dbc è esso stesso una tabella DBF: il suo catalogo contiene righe con
// OBJECTTYPE in {Database, Table, Index, Field, Relation, View}. Ci interessano i 'Table'.
var options = new DbfDataReaderOptions
{
Encoding = info.Encoding,
SkipDeletedRecords = true
};
using var reader = new DbfDataReader.DbfDataReader(info.DbcPath!, options);
int typeIdx = -1, nameIdx = -1;
for (int i = 0; i < reader.DbfTable.Columns.Count; i++)
{
var col = reader.DbfTable.Columns[i].ColumnName.Trim();
if (col.Equals("OBJECTTYPE", StringComparison.OrdinalIgnoreCase)) typeIdx = i;
else if (col.Equals("OBJECTNAME", StringComparison.OrdinalIgnoreCase)) nameIdx = i;
}
if (typeIdx < 0 || nameIdx < 0)
yield break; // .dbc non nel formato atteso
while (reader.Read())
{
var type = reader.IsDBNull(typeIdx) ? null : reader.GetValue(typeIdx)?.ToString()?.Trim();
if (!string.Equals(type, "Table", StringComparison.OrdinalIgnoreCase))
continue;
var name = reader.IsDBNull(nameIdx) ? null : reader.GetValue(nameIdx)?.ToString()?.Trim();
if (!string.IsNullOrEmpty(name))
yield return name;
}
}
private static string MapColumnType(DbfColumn col)
{
return col.ColumnType switch
{
DbfColumnType.Character => $"Character({col.Length})",
DbfColumnType.WideCharacter => $"Varchar({col.Length})",
DbfColumnType.Memo => "Memo",
DbfColumnType.Number => col.DecimalCount > 0
? $"Numeric({col.Length},{col.DecimalCount})"
: $"Numeric({col.Length})",
DbfColumnType.Float => $"Float({col.Length},{col.DecimalCount})",
DbfColumnType.Double => "Double",
DbfColumnType.SignedLong => "Integer",
DbfColumnType.Currency => "Currency",
DbfColumnType.Date => "Date",
DbfColumnType.DateTime => "DateTime",
DbfColumnType.Boolean => "Logical",
DbfColumnType.General => "General",
_ => col.ColumnType.ToString()
};
}
private static Encoding ResolveEncoding(int? overrideCodePage, string? dbcPath, string folder)
{
// 1) Override esplicito dell'utente
if (overrideCodePage.HasValue)
{
var enc = TryGetEncoding(overrideCodePage.Value);
if (enc != null) return enc;
}
// 2) Auto-detect dal byte "language driver" dell'header (dbc o prima tabella della cartella)
try
{
string? sampleDbf = dbcPath;
if (sampleDbf == null && Directory.Exists(folder))
sampleDbf = Directory.EnumerateFiles(folder, "*.dbf", SearchOption.TopDirectoryOnly).FirstOrDefault();
if (sampleDbf != null && File.Exists(sampleDbf))
{
using var fs = File.OpenRead(sampleDbf);
var header = new DbfHeader(fs);
if (LanguageDriverCodePages.TryGetValue(header.LanguageDriver, out var cp))
{
var enc = TryGetEncoding(cp);
if (enc != null) return enc;
}
}
}
catch
{
// ignora: si usa il default
}
// 3) Default
return Encoding.GetEncoding(DefaultCodePage);
}
private static Encoding? TryGetEncoding(int codePage)
{
try { return Encoding.GetEncoding(codePage); }
catch { return null; }
}
private static int? TryGetCodePageByName(string name)
{
try { return Encoding.GetEncoding(name).CodePage; }
catch { return null; }
}
}