using System.Data.Common; using System.Text.Json; using System.Text.RegularExpressions; using Omika.Data.Configuration; using Omika.Data.HfSql.Connection; namespace HfSqlOleDbAudit; /// /// Mode connection-test : valide les trois connexions HFSQL de Production /// (OLE DB PCSOFT.HFSQL) en LECTURE SEULE, sans démarrer Omika.Api ni /// Omika.Worker. /// /// /// Parité Production exacte : la chaîne de connexion OLE DB est construite par /// (Create / CreateForLicense / /// CreateForService), et /// — les mêmes classes que /// BusinessOleDbConnection, LicenseOleDbConnection et /// ServiceOleDbConnection. Les mots de passe de fichiers /// (ProtectionStd / ProtectionPlus) sont injectés dans les Extended Properties /// par le mécanisme Production (réflexion sur HfSqlTableAttribute), /// jamais dérivés, jamais calculés (aucun MD5, aucun wildcard improvisé). /// /// /// /// Sécurité : lecture seule stricte (aucune commande d'écriture de données ni de /// schéma), aucune écriture de fichier, aucun secret en argument CLI, aucun /// secret affiché (seuls des indicateurs YES/NO et un code d'erreur numérique /// sont émis). Aucune chaîne de connexion complète n'est jamais affichée. /// /// public static class ConnectionTest { public const string CommandName = "connection-test"; private static readonly Regex HfSqlErrorCodeRegex = new( @"Code\s+erreur\s*:\s*(\d+)|Error\s+code\s*:\s*(\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); /// Sections de configuration supportées (strictement). public enum Section { Hfsql, LicenseConnection, ServiceConnection } /// Arguments CLI validés du mode connection-test. public sealed record Arguments(string ConfigPath, Section SectionValue, string? Table); /// /// Configuration de connexion résolue depuis appsettings.Production.json, /// avec la vue OLE DB Production (). /// Ne contient jamais les valeurs secrètes en clair dans sa représentation. /// public sealed record Config( Section SectionValue, string Server, int Port, string DatabaseName, string Login, bool DatabasePasswordPresent, bool ProtectionStdPresent, bool ProtectionPlusPresent, HfSqlConnectionOptions ConnectionOptions); // ================================================================ // ENTRY // ================================================================ public static int Run(string[] args) => Run(args, Console.Out); public static int Run(string[] args, TextWriter output) { ArgumentNullException.ThrowIfNull(args); ArgumentNullException.ThrowIfNull(output); if (!TryParseArguments(args, out Arguments? arguments, out string? argError) || arguments is null) { output.WriteLine("ERROR=" + argError); PrintUsage(output); return 2; } if (!TryLoadConfig(arguments.ConfigPath, arguments.SectionValue, out Config? config, out string? configError) || config is null) { output.WriteLine("ERROR=" + configError); return 2; } foreach (string line in BuildSummaryLines(config)) { output.WriteLine(line); } var factory = new HfSqlOleDbConnectionFactory(); // La connexion est créée puis TOUJOURS disposée (y compris si l'ouverture // échoue) : le provider OLE DB natif PCSOFT.HFSQL doit être libéré // proprement, sinon le processus peut se terminer anormalement à la // finalisation de l'objet non disposé. DbConnection connection; try { connection = factory.Create(config.ConnectionOptions); } catch (Exception ex) { output.WriteLine("CONNECTION_OPEN=FAIL"); output.WriteLine("ERROR_TYPE=" + SanitizeErrorType(ex)); output.WriteLine("HFSQL_ERROR_CODE=" + (ExtractHfSqlErrorCode(ex) ?? "NONE")); return 1; } using (connection) { try { connection.Open(); } catch (Exception ex) { // Jamais de message brut : uniquement le type et le code numérique. output.WriteLine("CONNECTION_OPEN=FAIL"); output.WriteLine("ERROR_TYPE=" + SanitizeErrorType(ex)); output.WriteLine("HFSQL_ERROR_CODE=" + (ExtractHfSqlErrorCode(ex) ?? "NONE")); return 1; } output.WriteLine("CONNECTION_OPEN=PASS"); if (arguments.Table is not null) { bool ok = TrySelect(connection, arguments.Table, out int fieldCount, out bool hasRows, out string? code); output.WriteLine("TABLE=" + arguments.Table); output.WriteLine("SELECT=" + (ok ? "PASS" : "FAIL")); output.WriteLine("FIELD_COUNT=" + (ok ? fieldCount.ToString() : "0")); output.WriteLine("HAS_ROWS=" + (ok && hasRows ? "YES" : "NO")); output.WriteLine("HFSQL_ERROR_CODE=" + (code ?? "NONE")); if (!ok) { return 1; } } } return 0; } // ================================================================ // ARGUMENT PARSING // ================================================================ public static bool TryParseArguments(IReadOnlyList args, out Arguments? arguments, out string? error) { arguments = null; error = null; // args[0] = "connection-test", args[1] = , args[2] =
, args[3]? = . if (args.Count < 3) { error = "INVALID_ARGUMENTS"; return false; } if (!TryParseSection(args[2], out Section section)) { error = "UNSUPPORTED_SECTION"; return false; } string? table = null; if (args.Count >= 4) { table = args[3]; if (!IsValidTableIdentifier(table)) { error = "INVALID_TABLE_IDENTIFIER"; return false; } } arguments = new Arguments(args[1], section, table); return true; } public static bool TryParseSection(string section, out Section parsed) { if (section.Equals("Hfsql", StringComparison.OrdinalIgnoreCase)) { parsed = Section.Hfsql; return true; } if (section.Equals("LicenseConnection", StringComparison.OrdinalIgnoreCase)) { parsed = Section.LicenseConnection; return true; } if (section.Equals("ServiceConnection", StringComparison.OrdinalIgnoreCase)) { parsed = Section.ServiceConnection; return true; } parsed = default; return false; } // ================================================================ // CONFIG LOADING (lecture seule) // ================================================================ public static bool TryLoadConfig(string configPath, Section section, out Config? config, out string? error) { config = null; error = null; string json; try { json = File.ReadAllText(configPath); } catch (Exception) { error = "CONFIG_FILE_NOT_FOUND"; return false; } JsonDocument document; try { document = JsonDocument.Parse(json); } catch (JsonException) { error = "CONFIG_PARSE_FAILED"; return false; } using (document) { string sectionName = SectionName(section); if (!document.RootElement.TryGetProperty(sectionName, out JsonElement sectionElement) || sectionElement.ValueKind != JsonValueKind.Object) { error = "SECTION_NOT_FOUND"; return false; } var jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; HfSqlConnectionOptions connectionOptions; string server; int port; string databaseName; string login; try { switch (section) { case Section.Hfsql: { var options = sectionElement.Deserialize(jsonOptions) ?? new HfsqlOptions(); connectionOptions = HfSqlConnectionOptionsFactory.Create(options, options.FilePasswords); server = options.Server ?? string.Empty; port = options.Port; databaseName = options.DatabaseName ?? string.Empty; login = options.Login ?? string.Empty; break; } case Section.LicenseConnection: { var options = sectionElement.Deserialize(jsonOptions) ?? new LicenseConnectionOptions(); connectionOptions = HfSqlConnectionOptionsFactory.CreateForLicense(options); server = options.Server ?? string.Empty; port = options.Port; databaseName = options.DatabaseName ?? string.Empty; login = options.Login ?? string.Empty; break; } case Section.ServiceConnection: { var options = sectionElement.Deserialize(jsonOptions) ?? new ServiceConnectionOptions(); connectionOptions = HfSqlConnectionOptionsFactory.CreateForService(options); server = options.Server ?? string.Empty; port = options.Port; databaseName = options.DatabaseName ?? string.Empty; login = options.Login ?? string.Empty; break; } default: error = "UNSUPPORTED_SECTION"; return false; } } catch (Exception) { error = "CONFIG_PARSE_FAILED"; return false; } config = new Config( section, server, port, databaseName, login, !string.IsNullOrWhiteSpace(connectionOptions.Credential), !string.IsNullOrWhiteSpace(connectionOptions.FilePasswords.ProtectionStd), !string.IsNullOrWhiteSpace(connectionOptions.FilePasswords.ProtectionPlus), connectionOptions); return true; } } private static string SectionName(Section section) => section switch { Section.Hfsql => HfsqlOptions.SectionName, Section.LicenseConnection => LicenseConnectionOptions.SectionName, Section.ServiceConnection => ServiceConnectionOptions.SectionName, _ => throw new ArgumentOutOfRangeException(nameof(section)) }; // ================================================================ // SUMMARY (jamais de secret) // ================================================================ public static IReadOnlyList BuildSummaryLines(Config config) { ArgumentNullException.ThrowIfNull(config); return new[] { "Section=" + SectionName(config.SectionValue), "Server=" + config.Server, "Port=" + config.Port.ToString(), "DatabaseName=" + config.DatabaseName, "Login=" + config.Login, "Provider=" + HfSqlConnectionOptions.OleDbProvider, "DatabasePasswordPresent=" + (config.DatabasePasswordPresent ? "YES" : "NO"), "ProtectionStdPresent=" + (config.ProtectionStdPresent ? "YES" : "NO"), "ProtectionPlusPresent=" + (config.ProtectionPlusPresent ? "YES" : "NO") }; } // ================================================================ // SELECT (lecture seule) // ================================================================ public static bool IsValidTableIdentifier(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; } /// Unique requête autorisée : SELECT TOP 1 * FROM <table>. public static string BuildSelectSql(string table) => $"SELECT TOP 1 * FROM {table}"; private static bool TrySelect( DbConnection connection, string table, out int fieldCount, out bool hasRows, out string? hfSqlErrorCode) { fieldCount = 0; hasRows = false; hfSqlErrorCode = null; if (!IsValidTableIdentifier(table)) { return false; } try { using var command = connection.CreateCommand(); command.CommandText = BuildSelectSql(table); using var reader = command.ExecuteReader(); fieldCount = reader.FieldCount; hasRows = reader.Read(); return true; } catch (Exception ex) { hfSqlErrorCode = ExtractHfSqlErrorCode(ex); return false; } } // ================================================================ // SANITIZATION (aucun message brut, aucun secret) // ================================================================ public static string SanitizeErrorType(Exception ex) => ex.GetType().Name; public static string? ExtractHfSqlErrorCode(Exception ex) { var match = HfSqlErrorCodeRegex.Match(ex.Message); if (!match.Success) { return null; } return !string.IsNullOrEmpty(match.Groups[1].Value) ? match.Groups[1].Value : match.Groups[2].Value; } private static void PrintUsage(TextWriter output) { output.WriteLine("Usage: HfSqlOleDbAudit connection-test [
]"); } }