using System.Data.OleDb; using System.Reflection; using System.Text.Json; using System.Text.RegularExpressions; using Omika.Data.HfSql.Protection; namespace HfSqlOleDbAudit; internal static class Program { private static int Main(string[] args) { // Mode "connection-test" (lecture seule, OLE DB PCSOFT.HFSQL). Dispatch // ajouté sans modifier le comportement historique ci-dessous : tout autre // premier argument emprunte exactement le chemin d'origine. if (args.Length > 0 && StringComparer.OrdinalIgnoreCase.Equals(args[0], ConnectionTest.CommandName)) { return ConnectionTest.Run(args); } if (args.Length < 2) { Console.WriteLine("Usage: HfSqlOleDbAudit "); return 1; } string configPath = args[0]; string schema = args[1]; // ================================================================ // CONFIG LOADING // ================================================================ TestConfig? config; try { var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; config = JsonSerializer.Deserialize(File.ReadAllText(configPath), options); } catch { config = null; } if (config is null) { Console.WriteLine("Config JSON: INVALID"); return 1; } Console.WriteLine("Config JSON: VALID"); if (string.IsNullOrWhiteSpace(config.ProtectionPlusSecret)) { Console.WriteLine("ProtectionPlusSecret: MISSING"); return 1; } Console.WriteLine("ProtectionPlusSecret: AVAILABLE"); // ================================================================ // DATABASE CHOICE // ================================================================ DatabaseConfig db = schema.Equals("Batik", StringComparison.OrdinalIgnoreCase) ? config.Batik : config.Omika; string connectionDatabase = db.Database; string passwordDatabaseName = db.OriginalDatabaseName ?? db.Database; Console.WriteLine("PasswordDatabaseNameSource: " + (db.OriginalDatabaseName is null ? "CURRENT" : "ORIGINAL")); // ================================================================ // EXPLICIT LOCAL CONFIG — file passwords come from the local config, // NOT derived. No derivation, no wildcard. // ================================================================ string protectionStd = config.FilePasswords.ProtectionStd; string protectionPlus = config.FilePasswords.ProtectionPlus; Console.WriteLine("PasswordSource: EXPLICIT_LOCAL_CONFIG"); Console.WriteLine("PasswordDerivationUsed: NO"); Console.WriteLine("ProtectionPlusSecretUsedForConnection: NO"); // ================================================================ // LOAD TABLES // ================================================================ List tables = LoadTables(schema); var results = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var t in tables) { results[t.LogicalName] = new TableResult { Table = t }; } // ================================================================ // PHASE 1 — DIRECT // ================================================================ using (var cn = new OleDbConnection(BuildConnectionString(config, db, null))) { cn.Open(); foreach (var t in tables) { var r = results[t.LogicalName]; QueryResult qr = TrySelect(cn, t.LogicalName); if (qr.Success) { r.DirectAccess = true; r.FinalAccess = FinalAccess.Direct; r.FieldCount = qr.FieldCount; r.HasRows = qr.HasRows; } else { r.DirectAccess = false; r.FinalAccess = FinalAccess.Unknown; r.HfSqlErrorCode = qr.HfSqlErrorCode; } } } // ================================================================ // PHASE 2 — FAILURES-ONLY EXPLICIT MAP // ================================================================ string strategy = "FAILURES_ONLY_EXPLICIT_MAP"; List? workingEntries = null; var failedProtected = tables .Where(t => !results[t.LogicalName].DirectAccess && (t.ExpectedGroup == ExpectedGroup.ProtectionStd || t.ExpectedGroup == ExpectedGroup.ProtectionPlus)) .ToList(); if (failedProtected.Count > 0) { var entries = new List(); foreach (var t in failedProtected) { if (t.ExpectedGroup == ExpectedGroup.ProtectionStd) entries.Add(BuildFilePasswordEntry(t.LogicalName, protectionStd)); else entries.Add(BuildFilePasswordEntry(t.LogicalName, protectionPlus)); } string ext = string.Join(";", entries); using (var cn = new OleDbConnection(BuildConnectionString(config, db, ext))) { cn.Open(); foreach (var t in failedProtected) { var r = results[t.LogicalName]; QueryResult qr = TrySelect(cn, t.LogicalName); if (qr.Success) { r.ProtectedAccess = true; r.FinalAccess = t.ExpectedGroup == ExpectedGroup.ProtectionStd ? FinalAccess.ProtectionStd : FinalAccess.ProtectionPlus; r.FieldCount = qr.FieldCount; r.HasRows = qr.HasRows; } else { r.HfSqlErrorCode = qr.HfSqlErrorCode; } } } workingEntries = entries; } // ================================================================ // CANARY CHECK — if ParametreMetier passes, STD is verified. // ================================================================ bool stdVerified = false; bool plusVerified = false; if (results.TryGetValue("ParametreMetier", out var pmFinal) && !pmFinal.DirectAccess && pmFinal.ProtectedAccess) { stdVerified = true; } if ((results.TryGetValue("ParametreMetierEntete", out var pmeFinal) && !pmeFinal.DirectAccess && pmeFinal.ProtectedAccess) || (results.TryGetValue("ParametreSysteme", out var psFinal) && !psFinal.DirectAccess && psFinal.ProtectedAccess)) { plusVerified = true; } // ================================================================ // STRATEGY 2 — FULL EXPLICIT MAP (if ParametreMetier still failed) // ================================================================ bool pmStillFailed = results.TryGetValue("ParametreMetier", out var pmCheck) && pmCheck.FinalAccess != FinalAccess.Direct && pmCheck.FinalAccess != FinalAccess.ProtectionStd; if (pmStillFailed) { strategy = "FULL_EXPLICIT_MAP"; var entries = new List(); foreach (var t in tables) { if (t.ExpectedGroup == ExpectedGroup.ProtectionStd) entries.Add(BuildFilePasswordEntry(t.LogicalName, protectionStd)); else if (t.ExpectedGroup == ExpectedGroup.ProtectionPlus) entries.Add(BuildFilePasswordEntry(t.LogicalName, protectionPlus)); } string ext = string.Join(";", entries); using (var cn = new OleDbConnection(BuildConnectionString(config, db, ext))) { cn.Open(); foreach (var name in new[] { "Client", "ParametreMetier", "ParametreMetierEntete", "ParametreSysteme" }) { if (results.TryGetValue(name, out var r) && !r.DirectAccess) { QueryResult qr = TrySelect(cn, name); if (qr.Success) { r.ProtectedAccess = true; r.FinalAccess = r.Table.ExpectedGroup == ExpectedGroup.ProtectionStd ? FinalAccess.ProtectionStd : FinalAccess.ProtectionPlus; r.FieldCount = qr.FieldCount; r.HasRows = qr.HasRows; } else { r.HfSqlErrorCode = qr.HfSqlErrorCode; } } } foreach (var t in tables.Where(t => !results[t.LogicalName].DirectAccess && results[t.LogicalName].FinalAccess == FinalAccess.Unknown)) { var r = results[t.LogicalName]; QueryResult qr = TrySelect(cn, t.LogicalName); if (qr.Success) { r.ProtectedAccess = true; r.FinalAccess = t.ExpectedGroup == ExpectedGroup.ProtectionStd ? FinalAccess.ProtectionStd : FinalAccess.ProtectionPlus; r.FieldCount = qr.FieldCount; r.HasRows = qr.HasRows; } else { r.HfSqlErrorCode = qr.HfSqlErrorCode; } } } workingEntries = entries; // Re-evaluate canaries after strategy 2 if (results.TryGetValue("ParametreMetier", out var pm2) && !pm2.DirectAccess && pm2.ProtectedAccess) { stdVerified = true; } if ((results.TryGetValue("ParametreMetierEntete", out var pme2) && !pme2.DirectAccess && pme2.ProtectedAccess) || (results.TryGetValue("ParametreSysteme", out var ps2) && !ps2.DirectAccess && ps2.ProtectedAccess)) { plusVerified = true; } } // ================================================================ // STRATEGY 3 — WILDCARD_PLUS_OVERRIDES (if ParametreMetier still failed) // ================================================================ bool pmStillFailed2 = results.TryGetValue("ParametreMetier", out var pmCheck2) && pmCheck2.FinalAccess != FinalAccess.Direct && pmCheck2.FinalAccess != FinalAccess.ProtectionStd; if (pmStillFailed2) { strategy = "WILDCARD_PLUS_OVERRIDES"; var entries = new List(); // Wildcard first: applies ProtectionStd to all files. entries.Add(BuildFilePasswordEntry("*", protectionStd)); // Then explicit overrides for each ProtectionPlus file. foreach (var t in tables.Where(t => t.ExpectedGroup == ExpectedGroup.ProtectionPlus)) { entries.Add(BuildFilePasswordEntry(t.LogicalName, protectionPlus)); } string ext = string.Join(";", entries); using (var cn = new OleDbConnection(BuildConnectionString(config, db, ext))) { cn.Open(); foreach (var name in new[] { "Client", "ParametreMetier", "ParametreMetierEntete", "ParametreSysteme" }) { if (results.TryGetValue(name, out var r) && !r.DirectAccess) { QueryResult qr = TrySelect(cn, name); if (qr.Success) { r.ProtectedAccess = true; r.FinalAccess = r.Table.ExpectedGroup == ExpectedGroup.ProtectionStd ? FinalAccess.ProtectionStd : FinalAccess.ProtectionPlus; r.FieldCount = qr.FieldCount; r.HasRows = qr.HasRows; } else { r.HfSqlErrorCode = qr.HfSqlErrorCode; } } } foreach (var t in tables.Where(t => !results[t.LogicalName].DirectAccess && results[t.LogicalName].FinalAccess == FinalAccess.Unknown)) { var r = results[t.LogicalName]; QueryResult qr = TrySelect(cn, t.LogicalName); if (qr.Success) { r.ProtectedAccess = true; r.FinalAccess = t.ExpectedGroup == ExpectedGroup.ProtectionStd ? FinalAccess.ProtectionStd : FinalAccess.ProtectionPlus; r.FieldCount = qr.FieldCount; r.HasRows = qr.HasRows; } else { r.HfSqlErrorCode = qr.HfSqlErrorCode; } } } workingEntries = entries; // Re-evaluate canaries after strategy 3 if (results.TryGetValue("ParametreMetier", out var pm3) && !pm3.DirectAccess && pm3.ProtectedAccess) { stdVerified = true; } if ((results.TryGetValue("ParametreMetierEntete", out var pme3) && !pme3.DirectAccess && pme3.ProtectedAccess) || (results.TryGetValue("ParametreSysteme", out var ps3) && !ps3.DirectAccess && ps3.ProtectedAccess)) { plusVerified = true; } } // ================================================================ // NEGATIVE CONTROL // ================================================================ bool negativeControlRun = false; bool negativeControlPass = false; var protectedPassed = tables.FirstOrDefault(t => results[t.LogicalName].ProtectedAccess); if (protectedPassed is not null && workingEntries is not null) { negativeControlRun = true; string fakePassword = "00000000000000000000000000000000"; string chosenName = protectedPassed.LogicalName; var fakeEntries = new List(workingEntries); string escapedChosen = EscapeExtendedPropertyValue(chosenName); fakeEntries.RemoveAll(e => e.StartsWith("Password=" + escapedChosen + ":")); fakeEntries.Add(BuildFilePasswordEntry(chosenName, fakePassword)); string fakeExt = string.Join(";", fakeEntries); using (var cn = new OleDbConnection(BuildConnectionString(config, db, fakeExt))) { cn.Open(); QueryResult qr = TrySelect(cn, chosenName); negativeControlPass = !qr.Success; } } // ================================================================ // FINALIZE — mark any table still unresolved as Failed // ================================================================ foreach (var t in tables) { var r = results[t.LogicalName]; if (r.FinalAccess == FinalAccess.Unknown) { r.FinalAccess = FinalAccess.Failed; } } // ================================================================ // CONSOLE OUTPUT // ================================================================ Console.WriteLine(); Console.WriteLine($"=== {schema} ==="); Console.WriteLine(); foreach (var t in tables) { var r = results[t.LogicalName]; Console.WriteLine("LogicalFile: " + t.LogicalName); Console.WriteLine("ExpectedGroup: " + t.ExpectedGroup); Console.WriteLine("DirectAccess: " + r.DirectAccess); Console.WriteLine("ProtectedAccess: " + r.ProtectedAccess); Console.WriteLine("FinalAccess: " + GetFinalAccessLabel(r.FinalAccess)); Console.WriteLine("FieldCount: " + r.FieldCount); Console.WriteLine("HasRows: " + r.HasRows); Console.WriteLine("HfSqlErrorCode: " + (r.HfSqlErrorCode ?? "NONE")); Console.WriteLine(); } int directCount = tables.Count(t => results[t.LogicalName].DirectAccess); int stdCount = tables.Count(t => results[t.LogicalName].FinalAccess == FinalAccess.ProtectionStd); int plusCount = tables.Count(t => results[t.LogicalName].FinalAccess == FinalAccess.ProtectionPlus); int failedCount = tables.Count(t => results[t.LogicalName].FinalAccess == FinalAccess.Failed); Console.WriteLine("=== SUMMARY ==="); Console.WriteLine($"Tables: {tables.Count}"); Console.WriteLine($"Direct: {directCount}"); Console.WriteLine($"ProtectionStd: {stdCount}"); Console.WriteLine($"ProtectionPlus: {plusCount}"); Console.WriteLine($"Failed: {failedCount}"); Console.WriteLine(); Console.WriteLine($"ParametreMetier: {GetCanaryLabel(results, "ParametreMetier")}"); Console.WriteLine($"ParametreMetierEntete: {GetCanaryLabel(results, "ParametreMetierEntete")}"); Console.WriteLine($"ParametreSysteme: {GetCanaryLabel(results, "ParametreSysteme")}"); Console.WriteLine(); Console.WriteLine($"Strategy: {strategy}"); Console.WriteLine($"ProtectionStd OLE DB: {(stdVerified ? "VERIFIED_FROM_DATABASE" : "UNRESOLVED")}"); Console.WriteLine($"ProtectionPlus OLE DB: {(plusVerified ? "VERIFIED_FROM_DATABASE" : "UNRESOLVED")}"); Console.WriteLine($"NegativeControl: {(negativeControlRun ? (negativeControlPass ? "PASS" : "FAIL") : "NOT_RUN")}"); // ================================================================ // CSV REPORT // ================================================================ string csvPath = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "docs", "diagnostics", "hfsql-oledb-open-all.csv"); csvPath = Path.GetFullPath(csvPath); Directory.CreateDirectory(Path.GetDirectoryName(csvPath)!); bool csvExists = File.Exists(csvPath); var csvLines = new List(); if (!csvExists) { csvLines.Add("Schema,Table,DiskName,ExpectedGroup,DirectAccess,ProtectedAccess,FinalAccess,FieldCount,HasRows,HfSqlErrorCode"); } foreach (var t in tables) { var r = results[t.LogicalName]; csvLines.Add(string.Join(",", Csv(schema), Csv(t.LogicalName), Csv(t.DiskName), Csv(t.ExpectedGroup.ToString()), Csv(r.DirectAccess.ToString()), Csv(r.ProtectedAccess.ToString()), Csv(GetFinalAccessLabel(r.FinalAccess)), Csv(r.FieldCount.ToString()), Csv(r.HasRows.ToString()), Csv(r.HfSqlErrorCode ?? ""))); } File.AppendAllLines(csvPath, csvLines); Console.WriteLine($"CSV: CREATED ({csvPath})"); // ================================================================ // MARKDOWN REPORT // ================================================================ string mdPath = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "docs", "diagnostics", "hfsql-oledb-open-all.md"); mdPath = Path.GetFullPath(mdPath); Directory.CreateDirectory(Path.GetDirectoryName(mdPath)!); var md = new System.Text.StringBuilder(); bool mdExists = File.Exists(mdPath); if (!mdExists) { md.AppendLine("# HFSQL OLE DB Open All — Diagnostic"); md.AppendLine(); } md.AppendLine($"## {schema}"); md.AppendLine(); md.AppendLine("### Résumé"); md.AppendLine(); md.AppendLine($"- Tables: {tables.Count}"); md.AppendLine($"- Direct: {directCount}"); md.AppendLine($"- ProtectionStd: {stdCount}"); md.AppendLine($"- ProtectionPlus: {plusCount}"); md.AppendLine($"- Failed: {failedCount}"); md.AppendLine(); md.AppendLine($"- ParametreMetier: {GetCanaryLabel(results, "ParametreMetier")}"); md.AppendLine($"- ParametreMetierEntete: {GetCanaryLabel(results, "ParametreMetierEntete")}"); md.AppendLine($"- ParametreSysteme: {GetCanaryLabel(results, "ParametreSysteme")}"); md.AppendLine(); md.AppendLine($"- Strategy: {strategy}"); md.AppendLine($"- ProtectionStd OLE DB: {(stdVerified ? "VERIFIED_FROM_DATABASE" : "UNRESOLVED")}"); md.AppendLine($"- ProtectionPlus OLE DB: {(plusVerified ? "VERIFIED_FROM_DATABASE" : "UNRESOLVED")}"); md.AppendLine($"- NegativeControl: {(negativeControlRun ? (negativeControlPass ? "PASS" : "FAIL") : "NOT_RUN")}"); md.AppendLine(); md.AppendLine("### Canaris"); md.AppendLine(); md.AppendLine($"- ParametreMetier: {GetCanaryLabel(results, "ParametreMetier")}"); md.AppendLine($"- ParametreMetierEntete: {GetCanaryLabel(results, "ParametreMetierEntete")}"); md.AppendLine($"- ParametreSysteme: {GetCanaryLabel(results, "ParametreSysteme")}"); md.AppendLine(); md.AppendLine("### Échecs"); md.AppendLine(); foreach (var t in tables.Where(t => results[t.LogicalName].FinalAccess == FinalAccess.Failed)) { var r = results[t.LogicalName]; md.AppendLine($"- {t.LogicalName} (Expected={t.ExpectedGroup}, HfSql={r.HfSqlErrorCode ?? "NONE"})"); } md.AppendLine(); File.AppendAllText(mdPath, md.ToString()); Console.WriteLine($"MD: CREATED ({mdPath})"); return 0; } // ================================================================ // HELPERS // ================================================================ private static string GetCanaryLabel(Dictionary results, string name) { if (!results.TryGetValue(name, out var r)) return "FAILED"; return r.FinalAccess switch { FinalAccess.Direct => "DIRECT", FinalAccess.ProtectionStd => "PROTECTION_STD", FinalAccess.ProtectionPlus => "PROTECTION_PLUS", FinalAccess.Failed => "FAILED", _ => "FAILED" }; } private static string GetFinalAccessLabel(FinalAccess access) { return access switch { FinalAccess.Direct => "DIRECT", FinalAccess.ProtectionStd => "PROTECTION_STD", FinalAccess.ProtectionPlus => "PROTECTION_PLUS", FinalAccess.Failed => "FAILED", _ => "UNKNOWN" }; } private static string Csv(string? value) { if (string.IsNullOrEmpty(value)) return ""; if (value.Contains(',') || value.Contains('"') || value.Contains('\n')) return "\"" + value.Replace("\"", "\"\"") + "\""; return value; } // ================================================================ // MODEL LOADING (reflection) // ================================================================ private static List LoadTables(string schema) { var assembly = typeof(HfsqlFilePasswordDeriver).Assembly; string ns = schema.Equals("Batik", StringComparison.OrdinalIgnoreCase) ? "Omika.Data.HfSql.Models.Batik" : "Omika.Data.HfSql.Models.Omika"; var tables = new List(); foreach (var type in assembly.GetTypes()) { if (type.Namespace != ns) continue; var attr = type.GetCustomAttributes() .FirstOrDefault(a => a.GetType().Name == "HfSqlTableAttribute"); if (attr is null) continue; string tableName = GetAttrProperty(attr, "TableName") ?? type.Name; string diskName = GetAttrProperty(attr, "DiskName") ?? tableName + ".fic"; string[] groups = GetAttrGroups(attr); bool hasStd = groups.Any(g => g.Equals("ProtectionStd", StringComparison.OrdinalIgnoreCase)); bool hasPlus = groups.Any(g => g.Equals("ProtectionPlus", StringComparison.OrdinalIgnoreCase)); ExpectedGroup expected; if (hasStd && hasPlus) expected = ExpectedGroup.Conflict; else if (hasPlus) expected = ExpectedGroup.ProtectionPlus; else if (hasStd) expected = ExpectedGroup.ProtectionStd; else expected = ExpectedGroup.None; tables.Add(new TableInfo { LogicalName = tableName, DiskName = diskName, ExpectedGroup = expected }); } return tables.OrderBy(t => t.LogicalName).ToList(); } private static string? GetAttrProperty(object attr, string propName) { var prop = attr.GetType().GetProperty(propName); if (prop is null) return null; var val = prop.GetValue(attr); return val?.ToString(); } private static string[] GetAttrGroups(object attr) { var prop = attr.GetType().GetProperty("Groups"); if (prop is null) return []; var val = prop.GetValue(attr); if (val is string[] arr) return arr; if (val is IEnumerable en) return en.ToArray(); return []; } // ================================================================ // CONNECTION STRING / EXTENDED PROPERTIES // ================================================================ private static string EscapeExtendedPropertyValue(string value) { return value .Replace("\\", "\\\\") .Replace("=", "\\=") .Replace(";", "\\;") .Replace(":", "\\:"); } private static string BuildFilePasswordEntry(string logicalFileName, string password) { return "Password=" + EscapeExtendedPropertyValue(logicalFileName) + ":" + EscapeExtendedPropertyValue(password); } private static string BuildConnectionString( TestConfig root, DatabaseConfig database, string? extendedProperties) { var builder = new OleDbConnectionStringBuilder(); builder.Provider = "PCSOFT.HFSQL"; builder.DataSource = root.Server + ":" + root.Port; builder["Initial Catalog"] = database.Database; builder["User ID"] = database.User; builder["Password"] = database.Credential; if (!string.IsNullOrWhiteSpace(extendedProperties)) { builder["Extended Properties"] = extendedProperties; } return builder.ConnectionString; } // ================================================================ // QUERY // ================================================================ private static QueryResult TrySelect(OleDbConnection connection, string logicalName) { if (!IsValidIdentifier(logicalName)) { return new QueryResult(false, 0, false, null); } try { using var cmd = connection.CreateCommand(); cmd.CommandText = $"SELECT TOP 1 * FROM {logicalName}"; using var reader = cmd.ExecuteReader(); int fieldCount = reader.FieldCount; bool hasRows = reader.Read(); return new QueryResult(true, fieldCount, hasRows, null); } catch (OleDbException ex) { return new QueryResult(false, 0, false, ExtractHfSqlErrorCode(ex)); } } private static bool IsValidIdentifier(string name) { if (string.IsNullOrEmpty(name)) return false; foreach (char c in name) { bool ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_'; if (!ok) return false; } return true; } private static string? ExtractHfSqlErrorCode(OleDbException ex) { string msg = ex.Message; var m = Regex.Match(msg, @"Code\s+erreur\s*:\s*(\d+)"); if (!m.Success) m = Regex.Match(msg, @"Error\s+code\s*:\s*(\d+)"); return m.Success ? m.Groups[1].Value : null; } // ================================================================ // STRUCTURES // ================================================================ private sealed record QueryResult( bool Success, int FieldCount, bool HasRows, string? HfSqlErrorCode); private sealed class TestConfig { public int Port { get; set; } public string Server { get; set; } = ""; public string ProtectionPlusSecret { get; set; } = ""; public FilePasswordsConfig FilePasswords { get; set; } = new(); public DatabaseConfig Batik { get; set; } = new(); public DatabaseConfig Omika { get; set; } = new(); } private sealed class FilePasswordsConfig { public string ProtectionStd { get; set; } = ""; public string ProtectionPlus { get; set; } = ""; } private sealed class DatabaseConfig { public string Credential { get; set; } = ""; public string Database { get; set; } = ""; public string User { get; set; } = ""; public string? OriginalDatabaseName { get; set; } } private enum ExpectedGroup { None, ProtectionStd, ProtectionPlus, Conflict } private enum FinalAccess { Unknown, Direct, ProtectionStd, ProtectionPlus, Failed } private sealed class TableInfo { public string LogicalName { get; init; } = ""; public string DiskName { get; init; } = ""; public ExpectedGroup ExpectedGroup { get; init; } } private sealed class TableResult { public required TableInfo Table { get; init; } public bool DirectAccess { get; set; } public bool ProtectedAccess { get; set; } public FinalAccess FinalAccess { get; set; } public int FieldCount { get; set; } public bool HasRows { get; set; } public string? HfSqlErrorCode { get; set; } } }