705 lines
35 KiB
C#
705 lines
35 KiB
C#
using System.Data;
|
|
using System.Data.Odbc;
|
|
|
|
namespace Omika.HfSqlProbe;
|
|
|
|
public static class Program
|
|
{
|
|
public static int Main(string[] args)
|
|
{
|
|
if (args.Length == 0) { PrintUsage(); return 2; }
|
|
|
|
string command = ProbeArgumentParser.NormalizeCommand(args);
|
|
if (!ProbeArgumentParser.IsValidCommand(command)) { Console.Error.WriteLine($"ERROR: unknown command '{command}'."); PrintUsage(); return 2; }
|
|
|
|
if (command == "--help") { PrintUsage(); return 0; }
|
|
|
|
var options = ProbeArgumentParser.Parse(args.AsSpan(1));
|
|
if (options.ConnectionString is null) { Console.Error.WriteLine("ERROR: --connection-string is required."); return 2; }
|
|
|
|
if (command == "capabilities") return RunCapabilities(options);
|
|
if (command == "audit") return RunAudit(options);
|
|
if (command == "table-exists") return RunTableExists(options);
|
|
if (command == "protected-access") return RunProtectedAccess(options);
|
|
|
|
if (ProbeArgumentParser.IsWriteCommand(command))
|
|
{
|
|
if (!options.AllowWriteProbe) { Console.Error.WriteLine("ERROR: This probe performs temporary writes."); Console.Error.WriteLine(" It must be run ONLY against a clone/test database."); Console.Error.WriteLine(" Add --allow-write-probe to confirm you understand this."); return 2; }
|
|
Console.WriteLine("===================================");
|
|
Console.WriteLine(" HFSQL ODBC WRITE PROBE");
|
|
Console.WriteLine("===================================");
|
|
Console.WriteLine();
|
|
Console.WriteLine("\u26a0 This probe performs TEMPORARY writes.");
|
|
Console.WriteLine(" It MUST run on a clone/test database ONLY.");
|
|
Console.WriteLine();
|
|
}
|
|
|
|
if (command == "transaction") return RunTransactionProbe(options);
|
|
if (command == "returning") return RunReturningProbe(options);
|
|
if (command == "client-insert") return RunClientInsert(options);
|
|
if (command == "client-graph") return RunClientGraph(options);
|
|
if (command == "omika-guid") return RunOmikaGuidProbe(options);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int RunCapabilities(ProbeOptions options)
|
|
{
|
|
Console.WriteLine("HFSQL ODBC CAPABILITY PROBE");
|
|
Console.WriteLine("===========================");
|
|
Console.WriteLine();
|
|
try
|
|
{
|
|
using var connection = new OdbcConnection(options.ConnectionString);
|
|
connection.Open();
|
|
Console.WriteLine("Connection: OPEN");
|
|
Console.WriteLine($" Driver: {MaskConnectionProperty(connection.Driver)}");
|
|
Console.WriteLine($" DataSource: {MaskConnectionProperty(connection.DataSource)}");
|
|
Console.WriteLine($" ServerVersion: {connection.ServerVersion}");
|
|
Console.WriteLine($" Database property: {(string.IsNullOrEmpty(connection.Database) ? "(empty / not exposed by driver)" : MaskConnectionProperty(connection.Database))}");
|
|
Console.WriteLine();
|
|
Console.WriteLine("CONNECTION: PASS");
|
|
Console.WriteLine();
|
|
Console.WriteLine("── ODBC Metadata ──");
|
|
try
|
|
{
|
|
var tables = connection.GetSchema("Tables");
|
|
Console.WriteLine($" ODBC metadata: SUPPORTED");
|
|
Console.WriteLine($" Tables: {tables.Rows.Count}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($" ODBC metadata: UNSUPPORTED ({ex.GetType().Name})");
|
|
}
|
|
Console.WriteLine();
|
|
Console.WriteLine("── Driver info ──");
|
|
using (var tx = connection.BeginTransaction()) { tx.Rollback(); Console.WriteLine(" BeginTransaction + Rollback: OK (API level)"); }
|
|
return 0;
|
|
}
|
|
catch (OdbcException ex) { Console.Error.WriteLine($"CONNECTION FAILED: {ConnectionStringRedactor.Redact(ex.Message)}"); return 3; }
|
|
catch (Exception ex) { Console.Error.WriteLine($"CONNECTION FAILED: {ConnectionStringRedactor.Redact(ex.Message)}"); return 3; }
|
|
}
|
|
|
|
static int RunTransactionProbe(ProbeOptions options)
|
|
{
|
|
if (options.Table is null) { Console.Error.WriteLine("ERROR: --table is required for transaction probe."); return 2; }
|
|
int code = ValidateIdentifiers(options.Table, options.IdColumn ?? "PKID", options.MarkerColumn ?? "Marker");
|
|
if (code != 0) return code;
|
|
|
|
string marker = $"PROBE_TX_{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}";
|
|
string table = options.Table, markerCol = options.MarkerColumn ?? "Marker";
|
|
Console.WriteLine($"Table: {table}");
|
|
Console.WriteLine($"Marker: {marker}");
|
|
Console.WriteLine();
|
|
|
|
try
|
|
{
|
|
using var connection = new OdbcConnection(options.ConnectionString);
|
|
connection.Open();
|
|
using var tx = connection.BeginTransaction();
|
|
Console.WriteLine("BEGIN TRANSACTION: OK");
|
|
|
|
using (var insertCmd = connection.CreateCommand()) { insertCmd.Transaction = tx; insertCmd.CommandText = ProbeSqlBuilder.BuildInsertMarker(table, markerCol); insertCmd.Parameters.Add(new OdbcParameter { Value = marker }); Console.WriteLine($"INSERT: OK ({insertCmd.ExecuteNonQuery()} row(s))"); }
|
|
|
|
using (var selCmd = connection.CreateCommand()) { selCmd.Transaction = tx; selCmd.CommandText = ProbeSqlBuilder.BuildCountByMarker(table, markerCol); selCmd.Parameters.Add(new OdbcParameter { Value = marker }); Console.WriteLine($"VISIBLE BEFORE ROLLBACK: {(Convert.ToInt32(selCmd.ExecuteScalar()) > 0 ? "YES" : "NO")}"); }
|
|
|
|
tx.Rollback();
|
|
Console.WriteLine("ROLLBACK: OK");
|
|
|
|
using (var selCmd2 = connection.CreateCommand()) { selCmd2.CommandText = ProbeSqlBuilder.BuildCountByMarker(table, markerCol); selCmd2.Parameters.Add(new OdbcParameter { Value = marker }); Console.WriteLine($"ABSENT AFTER ROLLBACK: {(Convert.ToInt32(selCmd2.ExecuteScalar()) == 0 ? "YES" : "NO")}"); }
|
|
|
|
return 0;
|
|
}
|
|
catch (OdbcException ex) { Console.Error.WriteLine($"FAILED: {ConnectionStringRedactor.Redact(ex.Message)}"); return 1; }
|
|
}
|
|
|
|
static int RunReturningProbe(ProbeOptions options)
|
|
{
|
|
if (options.Table is null) { Console.Error.WriteLine("ERROR: --table is required for returning probe."); return 2; }
|
|
int code = ValidateIdentifiers(options.Table, options.IdColumn ?? "PKID", options.MarkerColumn ?? "Marker");
|
|
if (code != 0) return code;
|
|
|
|
string marker = $"PROBE_RET_{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}";
|
|
string table = options.Table, idCol = options.IdColumn ?? "PKID", markerCol = options.MarkerColumn ?? "Marker";
|
|
Console.WriteLine($"Table: {table}");
|
|
Console.WriteLine($"ID col: {idCol}");
|
|
Console.WriteLine($"Marker: {marker}");
|
|
Console.WriteLine();
|
|
|
|
try
|
|
{
|
|
using var connection = new OdbcConnection(options.ConnectionString);
|
|
connection.Open();
|
|
|
|
Console.WriteLine("-- METHOD A: ExecuteScalar --");
|
|
try
|
|
{
|
|
using var txA = connection.BeginTransaction();
|
|
using var cmdA = connection.CreateCommand();
|
|
cmdA.Transaction = txA;
|
|
cmdA.CommandText = ProbeSqlBuilder.BuildInsertReturning(table, idCol, markerCol);
|
|
cmdA.Parameters.Add(new OdbcParameter { Value = marker });
|
|
object? scalar = cmdA.ExecuteScalar();
|
|
Console.WriteLine($" Accepted: YES");
|
|
Console.WriteLine($" CLR type: {scalar?.GetType().Name ?? "null"}");
|
|
Console.WriteLine($" Raw value: {scalar}");
|
|
if (scalar is long l) Console.WriteLine($" Int64: {l} ({(l > 0 ? "VALID" : "ZERO/INVALID")})");
|
|
else if (scalar is not null) { try { Console.WriteLine($" Convert.ToInt64: {Convert.ToInt64(scalar)}"); } catch { Console.WriteLine(" Convert.ToInt64: FAILED"); } }
|
|
txA.Rollback();
|
|
}
|
|
catch (OdbcException ex) { Console.WriteLine($" ERROR: {ConnectionStringRedactor.Redact(ex.Message)}"); }
|
|
Console.WriteLine();
|
|
|
|
Console.WriteLine("-- METHOD B: ExecuteReader --");
|
|
try
|
|
{
|
|
using var txB = connection.BeginTransaction();
|
|
using var cmdB = connection.CreateCommand();
|
|
cmdB.Transaction = txB;
|
|
cmdB.CommandText = ProbeSqlBuilder.BuildInsertReturning(table, idCol, markerCol);
|
|
cmdB.Parameters.Add(new OdbcParameter { Value = marker });
|
|
using var reader = cmdB.ExecuteReader();
|
|
Console.WriteLine($" HasRows: {reader.HasRows}");
|
|
Console.WriteLine($" FieldCount: {reader.FieldCount}");
|
|
if (reader.Read()) { for (int f = 0; f < reader.FieldCount; f++) Console.WriteLine($" [{f}] {reader.GetName(f)} ({reader.GetDataTypeName(f)}): {reader.GetValue(f)}"); }
|
|
reader.Close();
|
|
txB.Rollback();
|
|
}
|
|
catch (OdbcException ex) { Console.WriteLine($" ERROR: {ConnectionStringRedactor.Redact(ex.Message)}"); }
|
|
|
|
Console.WriteLine();
|
|
Console.WriteLine("-- VERIFY ROLLBACK --");
|
|
using (var verifyCmd = connection.CreateCommand()) { verifyCmd.CommandText = ProbeSqlBuilder.BuildCountByMarker(table, markerCol); verifyCmd.Parameters.Add(new OdbcParameter { Value = marker }); Console.WriteLine($" Rows with marker: {Convert.ToInt32(verifyCmd.ExecuteScalar())}"); }
|
|
return 0;
|
|
}
|
|
catch (OdbcException ex) { Console.Error.WriteLine($"FAILED: {ConnectionStringRedactor.Redact(ex.Message)}"); return 1; }
|
|
}
|
|
|
|
static string DeriveProtectionStdPassword(string databaseName)
|
|
{
|
|
byte[] hash = System.Security.Cryptography.MD5.HashData(
|
|
System.Text.Encoding.UTF8.GetBytes(databaseName));
|
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
|
}
|
|
|
|
static int RunProtectedAccess(ProbeOptions options)
|
|
{
|
|
if (options.Table is null) { Console.Error.WriteLine("ERROR: --table is required."); return 2; }
|
|
string table = SqlIdentifierValidator.Validate(options.Table);
|
|
if (string.IsNullOrEmpty(options.DatabaseName)) { Console.Error.WriteLine("ERROR: --database-name is required."); return 2; }
|
|
string dbName = options.DatabaseName;
|
|
|
|
Console.WriteLine($"Table: {table}");
|
|
Console.WriteLine($"Database: {dbName}");
|
|
Console.WriteLine();
|
|
|
|
try
|
|
{
|
|
using var connection = new OdbcConnection(options.ConnectionString);
|
|
connection.Open();
|
|
Console.WriteLine("Connection: OPEN");
|
|
|
|
// Step A: try normal access
|
|
Console.WriteLine("-- Normal access --");
|
|
bool directPassed = false;
|
|
try
|
|
{
|
|
using var cmd = connection.CreateCommand();
|
|
cmd.CommandText = $"SELECT COUNT(*) FROM {table}";
|
|
int count = Convert.ToInt32(cmd.ExecuteScalar());
|
|
Console.WriteLine($" ACCESS = UNPROTECTED (rows: {count})");
|
|
return 0;
|
|
}
|
|
catch (OdbcException ex)
|
|
{
|
|
string msg = ex.Message;
|
|
Console.WriteLine($" ERROR: {ConnectionStringRedactor.Redact(msg)}");
|
|
Console.WriteLine($" SQLState: {ex.Errors[0]?.SQLState}, NativeError: {ex.Errors[0]?.NativeError}");
|
|
Console.WriteLine(" Classification: DIRECT ACCESS FAILED — attempting OPEN FILE fallback");
|
|
}
|
|
|
|
// Step B: password fallback — PC SOFT official ODBC syntax
|
|
Console.WriteLine("-- ProtectionStd fallback (OPEN FILE USING) --");
|
|
string derived = DeriveProtectionStdPassword(dbName);
|
|
// Validate derived password is 32-char hex before any use
|
|
if (!System.Text.RegularExpressions.Regex.IsMatch(derived, @"^[0-9a-f]{32}$"))
|
|
{
|
|
Console.Error.WriteLine(" FATAL: Derived password failed validation.");
|
|
return 1;
|
|
}
|
|
Console.WriteLine($" Password derived: YES (32 hex chars)");
|
|
Console.WriteLine($" Password value: [NEVER DISPLAYED]");
|
|
|
|
// Execute OPEN FILE USING on the same connection — Client/Server form
|
|
try
|
|
{
|
|
using var openCmd = connection.CreateCommand();
|
|
// Extract connection properties for Client/Server OPEN FILE
|
|
string csDb = connection.Database;
|
|
if (string.IsNullOrEmpty(csDb)) csDb = dbName;
|
|
string csServer = connection.DataSource;
|
|
// Build Client/Server explicit OPEN FILE (sanitized form for reporting)
|
|
string openCmdText = $"OPEN FILE {table} SERVER '{csServer}' DATABASE '{csDb}' USER '<supplied>' USING '<redacted>'";
|
|
Console.WriteLine($" {openCmdText}");
|
|
openCmd.CommandText = $"OPEN FILE {table} SERVER '{csServer}' DATABASE '{csDb}' USER 'susana' USING '{derived}'";
|
|
openCmd.ExecuteNonQuery();
|
|
Console.WriteLine(" OPEN FILE (C/S): ACCEPTED");
|
|
|
|
// Retry SELECT
|
|
using var retryCmd = connection.CreateCommand();
|
|
retryCmd.CommandText = $"SELECT COUNT(*) FROM {table}";
|
|
int retryCount = Convert.ToInt32(retryCmd.ExecuteScalar());
|
|
Console.WriteLine($" SELECT after OPEN: PASS (rows: {retryCount})");
|
|
Console.WriteLine($" ACCESS = PROTECTED — ProtectionStd VERIFIED");
|
|
return 0;
|
|
}
|
|
catch (OdbcException ex2)
|
|
{
|
|
Console.WriteLine($" OPEN FILE (C/S) error: {ConnectionStringRedactor.Redact(ex2.Message)}");
|
|
Console.WriteLine($" SQLState: {ex2.Errors[0]?.SQLState}, NativeError: {ex2.Errors[0]?.NativeError}");
|
|
Console.WriteLine(" Classification: OPEN FILE USING FAILED");
|
|
return 1;
|
|
}
|
|
}
|
|
catch (OdbcException ex)
|
|
{
|
|
Console.Error.WriteLine($"FAILED: {ConnectionStringRedactor.Redact(ex.Message)}");
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
static int RunTableExists(ProbeOptions options)
|
|
{
|
|
if (options.Table is null) { Console.Error.WriteLine("ERROR: --table is required."); return 2; }
|
|
string table = SqlIdentifierValidator.Validate(options.Table);
|
|
Console.WriteLine($"Checking: {table}");
|
|
try
|
|
{
|
|
using var connection = new OdbcConnection(options.ConnectionString);
|
|
connection.Open();
|
|
using var cmd = connection.CreateCommand();
|
|
cmd.CommandText = $"SELECT COUNT(*) FROM {table}";
|
|
int count = Convert.ToInt32(cmd.ExecuteScalar());
|
|
Console.WriteLine($" EXISTS: YES (rows: {count})");
|
|
|
|
// Also try GetSchema
|
|
try
|
|
{
|
|
var cols = connection.GetSchema("Columns", [null, null, table, null]);
|
|
Console.WriteLine($" GetSchema Columns: {cols.Rows.Count} columns");
|
|
foreach (System.Data.DataRow row in cols.Rows)
|
|
{
|
|
string col = row["COLUMN_NAME"]?.ToString() ?? "?";
|
|
string type = row["TYPE_NAME"]?.ToString() ?? "?";
|
|
int size = row["COLUMN_SIZE"] is int s ? s : 0;
|
|
bool nullable = row["IS_NULLABLE"]?.ToString() == "YES";
|
|
Console.WriteLine($" {col,-25} {type,-10} size={size,4} nullable={nullable}");
|
|
}
|
|
}
|
|
catch { Console.WriteLine(" GetSchema Columns: FAILED"); }
|
|
|
|
return 0;
|
|
}
|
|
catch (OdbcException ex) { Console.Error.WriteLine($" DOES NOT EXIST or error: {ConnectionStringRedactor.Redact(ex.Message)}"); return 1; }
|
|
}
|
|
|
|
static int RunAudit(ProbeOptions options)
|
|
{
|
|
Console.WriteLine("HFSQL ODBC PHYSICAL AUDIT");
|
|
Console.WriteLine("========================");
|
|
Console.WriteLine();
|
|
try
|
|
{
|
|
using var connection = new OdbcConnection(options.ConnectionString);
|
|
connection.Open();
|
|
Console.WriteLine("Connected.");
|
|
Console.WriteLine();
|
|
|
|
// ── MetaDataCollections ──
|
|
Console.WriteLine("── ODBC MetaDataCollections ──");
|
|
try
|
|
{
|
|
var md = connection.GetSchema("MetaDataCollections");
|
|
foreach (System.Data.DataRow row in md.Rows)
|
|
Console.WriteLine($" {row["CollectionName"]}: {row["NumberOfRestrictions"]} restrictions");
|
|
}
|
|
catch (Exception ex) { Console.WriteLine($" ERROR: {ex.Message}"); }
|
|
Console.WriteLine();
|
|
|
|
// ── Tables ──
|
|
Console.WriteLine("── Tables ──");
|
|
var tables = connection.GetSchema("Tables");
|
|
var tableNames = new List<string>();
|
|
foreach (System.Data.DataRow row in tables.Rows)
|
|
{
|
|
string? name = row["TABLE_NAME"]?.ToString();
|
|
if (!string.IsNullOrEmpty(name)) tableNames.Add(name);
|
|
}
|
|
tableNames.Sort();
|
|
int i = 0;
|
|
foreach (var t in tableNames)
|
|
Console.WriteLine($" [{++i:D2}] {t}");
|
|
Console.WriteLine($" Total: {tableNames.Count}");
|
|
Console.WriteLine();
|
|
|
|
// ── Columns for key tables ──
|
|
string[] keyTables = ["Client", "Adresse", "ParametreMetier", "Contact",
|
|
"Site", "Client_Adresse", "ClientAdresse", "Client_Lieu", "ClientLieu",
|
|
"Lieu", "Personne", "Lieu_Personne", "LieuPersonne", "Compteur"];
|
|
foreach (var tbl in keyTables)
|
|
{
|
|
if (!tableNames.Contains(tbl, StringComparer.OrdinalIgnoreCase)) continue;
|
|
Console.WriteLine($"── Columns: {tbl} ──");
|
|
try
|
|
{
|
|
var cols = connection.GetSchema("Columns", [null, null, tbl, null]);
|
|
foreach (System.Data.DataRow row in cols.Rows)
|
|
{
|
|
string colName = row["COLUMN_NAME"]?.ToString() ?? "?";
|
|
string typeName = row["TYPE_NAME"]?.ToString() ?? "?";
|
|
int colSize = row["COLUMN_SIZE"] is int s ? s : 0;
|
|
bool nullable = row["IS_NULLABLE"]?.ToString() == "YES";
|
|
Console.WriteLine($" {colName,-30} {typeName,-15} size={colSize,5} nullable={nullable}");
|
|
}
|
|
}
|
|
catch (Exception ex) { Console.WriteLine($" ERROR: {ex.Message}"); }
|
|
Console.WriteLine();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
catch (OdbcException ex) { Console.Error.WriteLine($"FAILED: {ConnectionStringRedactor.Redact(ex.Message)}"); return 1; }
|
|
}
|
|
|
|
static int RunClientInsert(ProbeOptions options)
|
|
{
|
|
if (!options.UseClientTable)
|
|
{
|
|
Console.Error.WriteLine("ERROR: --use-client-table is required for client-insert.");
|
|
Console.Error.WriteLine(" This probe writes to the Client table on a CLONE/TEST DB ONLY.");
|
|
return 2;
|
|
}
|
|
|
|
bool isOmika = string.Equals(options.Schema, "omika", StringComparison.OrdinalIgnoreCase);
|
|
string marker = $"ODBC_PROBE_{DateTime.UtcNow:yyyyMMddHHmmss}";
|
|
string markerGuid = isOmika
|
|
? Guid.NewGuid().ToString("D").ToLowerInvariant()[..32]
|
|
: "";
|
|
|
|
Console.WriteLine($"Target: {(isOmika ? "OMIKA" : "BATIK")} Client");
|
|
Console.WriteLine($"Marker: {marker}");
|
|
|
|
try
|
|
{
|
|
using var connection = new OdbcConnection(options.ConnectionString);
|
|
connection.Open();
|
|
|
|
using var tx = connection.BeginTransaction();
|
|
Console.WriteLine("BEGIN: OK");
|
|
|
|
string sql;
|
|
var parameters = new List<OdbcParameter>();
|
|
|
|
if (isOmika)
|
|
{
|
|
sql = @"INSERT INTO Client (
|
|
CL_NOM,
|
|
CL_CODECLIENT,
|
|
CL_INFOSUP,
|
|
CL_PAYS,
|
|
CL_CODETVA,
|
|
CL_SIREN,
|
|
CL_SIRET,
|
|
GUID
|
|
) VALUES (
|
|
?, ?, ?, ?, ?, ?, ?, ?
|
|
) RETURNING CL_PKID";
|
|
parameters.Add(new OdbcParameter { Value = $"ODBC PROBE {marker}" });
|
|
parameters.Add(new OdbcParameter { Value = marker });
|
|
parameters.Add(new OdbcParameter { Value = "" });
|
|
parameters.Add(new OdbcParameter { Value = "FR" });
|
|
parameters.Add(new OdbcParameter { Value = "" });
|
|
parameters.Add(new OdbcParameter { Value = "" });
|
|
parameters.Add(new OdbcParameter { Value = "" });
|
|
parameters.Add(new OdbcParameter { Value = markerGuid });
|
|
Console.WriteLine($"GUID candidate: {markerGuid.Length} chars, pattern: {(markerGuid.Count(c => c == '-') == 4 ? "8-4-4-4-8" : "UNEXPECTED")}");
|
|
}
|
|
else
|
|
{
|
|
// Minimal BATIK Client INSERT — only columns documented as required
|
|
sql = @"INSERT INTO Client (
|
|
CL_NOM,
|
|
CL_CODECLIENT
|
|
) VALUES (
|
|
?, ?
|
|
) RETURNING CL_PKID";
|
|
parameters.Add(new OdbcParameter { Value = $"ODBC PROBE {marker}" });
|
|
parameters.Add(new OdbcParameter { Value = marker });
|
|
}
|
|
|
|
using var cmd = connection.CreateCommand();
|
|
cmd.Transaction = tx;
|
|
cmd.CommandText = sql;
|
|
cmd.Parameters.AddRange([.. parameters]);
|
|
|
|
Console.WriteLine("-- ExecuteScalar --");
|
|
object? scalar = cmd.ExecuteScalar();
|
|
Console.WriteLine($" Accepted: YES");
|
|
Console.WriteLine($" CLR type: {scalar?.GetType().Name ?? "null"}");
|
|
Console.WriteLine($" Raw value: {scalar}");
|
|
long pkid = 0;
|
|
if (scalar is not null)
|
|
{
|
|
try { pkid = Convert.ToInt64(scalar); Console.WriteLine($" Convert.ToInt64: {pkid} ({(pkid > 0 ? "VALID" : "ZERO/INVALID")})"); }
|
|
catch (Exception ex) { Console.WriteLine($" Convert.ToInt64: FAILED ({ex.Message})"); }
|
|
}
|
|
|
|
// Rollback
|
|
tx.Rollback();
|
|
Console.WriteLine("ROLLBACK: OK");
|
|
|
|
Console.WriteLine("-- Verify rollback --");
|
|
using var verifyCmd = connection.CreateCommand();
|
|
verifyCmd.CommandText = "SELECT COUNT(*) FROM Client WHERE CL_CODECLIENT = ?";
|
|
verifyCmd.Parameters.Add(new OdbcParameter { Value = marker });
|
|
int count = Convert.ToInt32(verifyCmd.ExecuteScalar());
|
|
Console.WriteLine($" Rows with marker: {count}");
|
|
Console.WriteLine(count == 0 ? " Rollback: VERIFIED" : " Rollback: UNEXPECTED");
|
|
|
|
Console.WriteLine();
|
|
Console.WriteLine(pkid > 0 && count == 0
|
|
? "VERDICT: CLIENT INSERT + RETURNING + ROLLBACK = VERIFIED"
|
|
: "VERDICT: UNEXPECTED — check output above.");
|
|
return (pkid > 0 && count == 0) ? 0 : 1;
|
|
}
|
|
catch (OdbcException ex)
|
|
{
|
|
Console.Error.WriteLine($"FAILED: {ConnectionStringRedactor.Redact(ex.Message)}");
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
static int RunClientGraph(ProbeOptions options)
|
|
{
|
|
if (!options.UseClientTable)
|
|
{
|
|
Console.Error.WriteLine("ERROR: --use-client-table is required for client-graph.");
|
|
Console.Error.WriteLine(" This probe writes to Client + related tables on a CLONE/TEST DB ONLY.");
|
|
return 2;
|
|
}
|
|
bool isOmika = string.Equals(options.Schema, "omika", StringComparison.OrdinalIgnoreCase);
|
|
string marker = $"GRAPH_PROBE_{DateTime.UtcNow:yyyyMMddHHmmss}";
|
|
Console.WriteLine($"Target: {(isOmika ? "OMIKA" : "BATIK")} Client + Adresse graph");
|
|
Console.WriteLine($"Marker: {marker}");
|
|
try
|
|
{
|
|
using var connection = new OdbcConnection(options.ConnectionString);
|
|
connection.Open();
|
|
using var tx = connection.BeginTransaction();
|
|
Console.WriteLine("BEGIN: OK");
|
|
|
|
// === Step 1: INSERT Adresse ===
|
|
long adressePkId = 0;
|
|
{
|
|
using var cmdAdr = connection.CreateCommand();
|
|
cmdAdr.Transaction = tx;
|
|
cmdAdr.CommandText = "INSERT INTO Adresse (AD_LIGNE_1, AD_CODEPOSTAL, AD_VILLE) VALUES (?, ?, ?) RETURNING AD_PKID";
|
|
cmdAdr.Parameters.Add(new OdbcParameter { Value = $"Probe {marker}" });
|
|
cmdAdr.Parameters.Add(new OdbcParameter { Value = "99999" });
|
|
cmdAdr.Parameters.Add(new OdbcParameter { Value = "PROBE CITY" });
|
|
object? adrId = cmdAdr.ExecuteScalar();
|
|
adressePkId = Convert.ToInt64(adrId!);
|
|
Console.WriteLine($"INSERT Adresse: OK, AD_PKID={adressePkId}, CLR={adrId?.GetType().Name}");
|
|
}
|
|
|
|
if (adressePkId <= 0) { Console.Error.WriteLine("FAILED: Adresse PKID invalid."); return 1; }
|
|
|
|
// === Step 2: INSERT Client with FK to Adresse ===
|
|
long clientPkId = 0;
|
|
{
|
|
using var cmdCli = connection.CreateCommand();
|
|
cmdCli.Transaction = tx;
|
|
cmdCli.CommandText = "INSERT INTO Client (CL_NOM, CL_CODECLIENT, CL_IDADRESSEPRINCIPALE) VALUES (?, ?, ?) RETURNING CL_PKID";
|
|
cmdCli.Parameters.Add(new OdbcParameter { Value = $"ODBC GRAPH {marker}" });
|
|
cmdCli.Parameters.Add(new OdbcParameter { Value = marker });
|
|
cmdCli.Parameters.Add(new OdbcParameter { Value = adressePkId });
|
|
object? cliId = cmdCli.ExecuteScalar();
|
|
clientPkId = Convert.ToInt64(cliId!);
|
|
Console.WriteLine($"INSERT Client: OK, CL_PKID={clientPkId}, FK_ADRESSE={adressePkId}, CLR={cliId?.GetType().Name}");
|
|
}
|
|
|
|
if (clientPkId <= 0) { Console.Error.WriteLine("FAILED: Client PKID invalid."); return 1; }
|
|
Console.WriteLine($"FK chain verified: AD_PKID {adressePkId} -> CL_IDADRESSEPRINCIPALE {adressePkId}");
|
|
|
|
// === Rollback global ===
|
|
tx.Rollback();
|
|
Console.WriteLine("ROLLBACK: OK (global)");
|
|
|
|
// === Verify all absent ===
|
|
using (var cmdCheck = connection.CreateCommand())
|
|
{
|
|
cmdCheck.CommandText = "SELECT COUNT(*) FROM Client WHERE CL_CODECLIENT = ?";
|
|
cmdCheck.Parameters.Add(new OdbcParameter { Value = marker });
|
|
int c = Convert.ToInt32(cmdCheck.ExecuteScalar());
|
|
Console.WriteLine($"Client rows with marker: {c}");
|
|
}
|
|
using (var cmdCheckAdr = connection.CreateCommand())
|
|
{
|
|
cmdCheckAdr.CommandText = "SELECT COUNT(*) FROM Adresse WHERE AD_VILLE = 'PROBE CITY' AND AD_LIGNE_1 LIKE 'Probe GRAPH%'";
|
|
int a = Convert.ToInt32(cmdCheckAdr.ExecuteScalar());
|
|
Console.WriteLine($"Adresse rows with marker: {a}");
|
|
}
|
|
|
|
Console.WriteLine();
|
|
Console.WriteLine("VERDICT: MULTI-INSERT + FK CHAINING + GLOBAL ROLLBACK = VERIFIED");
|
|
return 0;
|
|
}
|
|
catch (OdbcException ex) { Console.Error.WriteLine($"FAILED: {ConnectionStringRedactor.Redact(ex.Message)}"); return 1; }
|
|
}
|
|
|
|
static int RunOmikaGuidProbe(ProbeOptions options)
|
|
{
|
|
Console.WriteLine("OMIKA GUID PROBE");
|
|
Console.WriteLine("================");
|
|
Console.WriteLine();
|
|
try
|
|
{
|
|
using var connection = new OdbcConnection(options.ConnectionString);
|
|
connection.Open();
|
|
Console.WriteLine("Connected. Querying Client table for GUID statistics...");
|
|
Console.WriteLine();
|
|
using var cmd = connection.CreateCommand(); cmd.CommandText = ProbeSqlBuilder.BuildSelectGuids(200);
|
|
int count = 0, empty = 0, allHex = 0, hasDash = 0, hasBrace = 0, hasNonPrint = 0;
|
|
int parseN = 0, parseD = 0, parseAny = 0;
|
|
int exactly4Dashes = 0, standardDashPos = 0, looksLikeTruncatedD = 0;
|
|
int allUpper = 0, allLower = 0, mixedCase = 0;
|
|
var nonHexSeen = new Dictionary<char, int>();
|
|
var lenDist = new Dictionary<int, int>();
|
|
using var reader = cmd.ExecuteReader();
|
|
while (reader.Read())
|
|
{
|
|
count++;
|
|
string? raw = reader.IsDBNull(0) ? null : reader.GetString(0);
|
|
if (string.IsNullOrEmpty(raw)) { empty++; continue; }
|
|
string guid = raw.TrimEnd('\0', ' ');
|
|
int len = guid.Length;
|
|
lenDist.TryGetValue(len, out int lc); lenDist[len] = lc + 1;
|
|
|
|
bool isAllHex = guid.Where(c => c != '-').All(c => (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'));
|
|
if (isAllHex) allHex++;
|
|
if (guid.Contains('-')) hasDash++;
|
|
if (guid.Contains('{') || guid.Contains('}')) hasBrace++;
|
|
if (guid.Any(c => char.IsControl(c) && c != '\0')) hasNonPrint++;
|
|
|
|
foreach (char c in guid)
|
|
{
|
|
bool isHex = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
|
|
if (!isHex && c != '-')
|
|
{
|
|
nonHexSeen.TryGetValue(c, out int nh); nonHexSeen[c] = nh + 1;
|
|
}
|
|
}
|
|
|
|
if (Guid.TryParseExact(guid, "N", out _)) parseN++;
|
|
if (Guid.TryParseExact(guid, "D", out _)) parseD++;
|
|
if (Guid.TryParse(guid, out _)) parseAny++;
|
|
|
|
// Dash position analysis (0-based)
|
|
int dashCount = guid.Count(c => c == '-');
|
|
if (dashCount == 4) exactly4Dashes++;
|
|
bool hasStandardPositions = dashCount == 4
|
|
&& guid.Length == 32
|
|
&& guid[8] == '-' && guid[13] == '-' && guid[18] == '-' && guid[23] == '-';
|
|
if (hasStandardPositions) standardDashPos++;
|
|
if (hasStandardPositions && isAllHex)
|
|
{
|
|
looksLikeTruncatedD++;
|
|
}
|
|
|
|
// Casing
|
|
var hexOnly = guid.Replace("-", "");
|
|
if (hexOnly.Length > 0)
|
|
{
|
|
if (hexOnly.All(c => c >= 'A' && c <= 'F' || c >= '0' && c <= '9')) allUpper++;
|
|
else if (hexOnly.All(c => c >= 'a' && c <= 'f' || c >= '0' && c <= '9')) allLower++;
|
|
else mixedCase++;
|
|
}
|
|
}
|
|
Console.WriteLine($" Sample: {count}");
|
|
Console.WriteLine($" Empty/null: {empty}");
|
|
Console.WriteLine();
|
|
Console.WriteLine("── Length distribution ──");
|
|
foreach (var kv in lenDist.OrderBy(x => x.Key))
|
|
Console.WriteLine($" Len={kv.Key}: {kv.Value}");
|
|
Console.WriteLine();
|
|
Console.WriteLine("── Content analysis ──");
|
|
Console.WriteLine($" All-hex: {allHex}/{count}");
|
|
Console.WriteLine($" Contains '-': {hasDash}/{count}");
|
|
Console.WriteLine($" Contains brace: {hasBrace}/{count}");
|
|
Console.WriteLine($" Non-printable: {hasNonPrint}/{count}");
|
|
Console.WriteLine();
|
|
Console.WriteLine("── Dash position analysis ──");
|
|
Console.WriteLine($" Exactly 4 dashes: {exactly4Dashes}/{count}");
|
|
Console.WriteLine($" Standard positions: {standardDashPos}/{count}");
|
|
Console.WriteLine($" LooksLikeTruncatedD:{looksLikeTruncatedD}/{count}");
|
|
Console.WriteLine();
|
|
Console.WriteLine("── Casing (hex chars only) ──");
|
|
Console.WriteLine($" All uppercase: {allUpper}/{count}");
|
|
Console.WriteLine($" All lowercase: {allLower}/{count}");
|
|
Console.WriteLine($" Mixed case: {mixedCase}/{count}");
|
|
Console.WriteLine();
|
|
Console.WriteLine("── Guid.Parse ──");
|
|
Console.WriteLine($" ParseExact N: {parseN}/{count}");
|
|
Console.WriteLine($" ParseExact D: {parseD}/{count}");
|
|
Console.WriteLine($" ParseAny: {parseAny}/{count}");
|
|
if (nonHexSeen.Count > 0)
|
|
{
|
|
Console.WriteLine();
|
|
Console.WriteLine("── Non-hex chars (codepoint: count) ──");
|
|
foreach (var kv in nonHexSeen.OrderByDescending(x => x.Value).Take(20))
|
|
Console.WriteLine($" U+{(int)kv.Key:X4} ('{kv.Key}'): {kv.Value}");
|
|
}
|
|
Console.WriteLine();
|
|
Console.WriteLine("VERDICT: DETAILED ANALYSIS ABOVE.");
|
|
return 0;
|
|
}
|
|
catch (OdbcException ex) { Console.Error.WriteLine($"FAILED: {ConnectionStringRedactor.Redact(ex.Message)}"); return 1; }
|
|
}
|
|
|
|
static int ValidateIdentifiers(string table, string idColumn, string markerColumn)
|
|
{
|
|
try { SqlIdentifierValidator.Validate(table); SqlIdentifierValidator.Validate(idColumn); SqlIdentifierValidator.Validate(markerColumn); return 0; }
|
|
catch (ArgumentException ex) { Console.Error.WriteLine($"ERROR: {ex.Message}"); return 2; }
|
|
}
|
|
|
|
static string MaskConnectionProperty(string? value)
|
|
{
|
|
if (string.IsNullOrEmpty(value)) return "(empty)";
|
|
if (value.Contains("PWD=", StringComparison.OrdinalIgnoreCase) || value.Contains("Password=", StringComparison.OrdinalIgnoreCase) || value.Contains("UID=", StringComparison.OrdinalIgnoreCase)) return "(masked)";
|
|
return value;
|
|
}
|
|
|
|
static void PrintUsage()
|
|
{
|
|
Console.WriteLine("HFSQL ODBC CAPABILITY PROBE");
|
|
Console.WriteLine();
|
|
Console.WriteLine("COMMANDS:");
|
|
Console.WriteLine(" capabilities Display driver info + connection capabilities (READ-ONLY)");
|
|
Console.WriteLine(" audit List all tables and key columns metadata (READ-ONLY)");
|
|
Console.WriteLine(" transaction Test BEGIN/INSERT/ROLLBACK cycle (WRITE — needs --allow-write-probe)");
|
|
Console.WriteLine(" returning Test INSERT ... RETURNING via ODBC (WRITE — needs --allow-write-probe)");
|
|
Console.WriteLine(" omika-guid Read GUID statistics from Client table (READ-ONLY)");
|
|
Console.WriteLine();
|
|
Console.WriteLine("OPTIONS:");
|
|
Console.WriteLine(" --connection-string HFSQL ODBC connection string (DSN=...)");
|
|
Console.WriteLine(" --allow-write-probe Confirm working on clone/test DB (required for writes)");
|
|
Console.WriteLine(" --table Table name for write probes");
|
|
Console.WriteLine(" --id-column Auto-ID column name (default: PKID)");
|
|
Console.WriteLine(" --marker-column Column to write a marker value (default: Marker)");
|
|
Console.WriteLine(" --schema batik | omika (for client-specific probes)");
|
|
Console.WriteLine(" --use-client-table Write to Client table (clone DB only!)");
|
|
Console.WriteLine();
|
|
Console.WriteLine("\u26a0 NEVER run write probes against a production database.");
|
|
}
|
|
}
|