using MMoneyCoderSharp.Data; namespace MBN_STOCK_WEBVIEW.Infrastructure; public sealed class DatabaseRuntime { private DatabaseRuntime( DatabaseOptions options, IDatabaseConnectionFactory connectionFactory, IDataQueryExecutor executor, IDatabaseHealthService healthService) { Options = options; ConnectionFactory = connectionFactory; Executor = executor; HealthService = healthService; } public DatabaseOptions Options { get; } public IDatabaseConnectionFactory ConnectionFactory { get; } public IDataQueryExecutor Executor { get; } public IDatabaseHealthService HealthService { get; } public static DatabaseRuntime CreateDefault(string? configurationPath = null) { var options = new DatabaseOptionsLoader().Load(configurationPath); return Create(options); } public static DatabaseRuntime CreateLegacyExecutableDefault( string baseDirectory, string? fallbackConfigurationPath = null, string? legacyOverridePath = null, bool preferLegacyOverride = false) { ArgumentException.ThrowIfNullOrWhiteSpace(baseDirectory); if (preferLegacyOverride && !string.IsNullOrWhiteSpace(legacyOverridePath) && File.Exists(legacyOverridePath)) { return Create(new LegacyIniDatabaseOptionsLoader().Load(legacyOverridePath)); } var legacyIniPath = Path.Combine( Path.GetFullPath(baseDirectory), "Res", "MmoneyCoder.ini"); if (File.Exists(legacyIniPath)) { return Create(new LegacyIniDatabaseOptionsLoader().Load(legacyIniPath)); } var localOverride = legacyOverridePath ?? ResolveLegacyOverridePath(); if (!string.IsNullOrWhiteSpace(localOverride) && File.Exists(localOverride)) { return Create(new LegacyIniDatabaseOptionsLoader().Load(localOverride)); } return CreateDefault(fallbackConfigurationPath); } /// /// Creates the Development Live database runtime from the one protected /// current-user LocalAppData legacy INI, or from the protected LocalAppData /// JSON when that INI is absent. Unlike the ordinary compatibility path, this /// never probes executable or operator-selected Res folders and ignores all /// database environment overrides. /// public static DatabaseRuntime CreateDevelopmentLiveLocalOverride() { var localApplicationData = Environment.GetFolderPath( Environment.SpecialFolder.LocalApplicationData); return CreateDevelopmentLiveLocalOverride(localApplicationData); } internal static DatabaseRuntime CreateDevelopmentLiveLocalOverride( string localApplicationData) { try { ArgumentException.ThrowIfNullOrWhiteSpace(localApplicationData); var localRoot = Path.TrimEndingDirectorySeparator( Path.GetFullPath(localApplicationData)); var resourceDirectory = Path.Combine( localRoot, "MBN_STOCK_WEBVIEW", "Res"); var legacyIniPath = Path.Combine(resourceDirectory, "MmoneyCoder.ini"); if (TryEnsureOrdinaryFile(legacyIniPath)) { EnsureNoReparsePointDirectoryChain(resourceDirectory, localRoot); return Create( new LegacyIniDatabaseOptionsLoader(_ => null).Load(legacyIniPath)); } var configurationDirectory = Path.Combine( localRoot, "MBN_STOCK_WEBVIEW", "Config"); EnsureNoReparsePointDirectoryChain(configurationDirectory, localRoot); var jsonPath = Path.Combine( configurationDirectory, "database.local.json"); EnsureOrdinaryFile(jsonPath); var options = new DatabaseOptionsLoader(_ => null).Load(jsonPath); if (!options.IsConfigured(MMoneyCoderSharp.Data.DataSourceKind.Oracle) || !options.IsConfigured(MMoneyCoderSharp.Data.DataSourceKind.MariaDb)) { throw new DatabaseConfigurationException( "Development Live database configuration is unavailable."); } return Create(options); } catch (DatabaseConfigurationException) { throw; } catch (Exception exception) when ( exception is ArgumentException or IOException or UnauthorizedAccessException or InvalidDataException or NotSupportedException or System.Security.SecurityException) { throw new DatabaseConfigurationException( "Development Live database configuration is unavailable."); } } private static string? ResolveLegacyOverridePath() { var localAppData = Environment.GetFolderPath( Environment.SpecialFolder.LocalApplicationData); return string.IsNullOrWhiteSpace(localAppData) ? null : Path.Combine( localAppData, "MBN_STOCK_WEBVIEW", "Res", "MmoneyCoder.ini"); } private static void EnsureNoReparsePointDirectoryChain( string directory, string trustedRoot) { var current = Path.TrimEndingDirectorySeparator(Path.GetFullPath(directory)); var root = Path.TrimEndingDirectorySeparator(Path.GetFullPath(trustedRoot)); while (!string.Equals(current, root, StringComparison.OrdinalIgnoreCase)) { if ((File.GetAttributes(current) & FileAttributes.ReparsePoint) != 0) { throw new InvalidDataException(); } var parent = Path.GetDirectoryName(current); if (string.IsNullOrWhiteSpace(parent) || string.Equals(parent, current, StringComparison.OrdinalIgnoreCase)) { throw new InvalidDataException(); } current = Path.TrimEndingDirectorySeparator(Path.GetFullPath(parent)); } } private static void EnsureOrdinaryFile(string path) { var attributes = File.GetAttributes(Path.GetFullPath(path)); if ((attributes & (FileAttributes.Directory | FileAttributes.ReparsePoint)) != 0) { throw new InvalidDataException(); } } private static bool TryEnsureOrdinaryFile(string path) { try { EnsureOrdinaryFile(path); return true; } catch (FileNotFoundException) { return false; } catch (DirectoryNotFoundException) { return false; } } public static DatabaseRuntime Create(DatabaseOptions options) { ArgumentNullException.ThrowIfNull(options); options.Normalize(); options.ValidateRuntimeSettings(); var connectionFactory = new ProviderDatabaseConnectionFactory(options); var executor = new ResilientDataQueryExecutor(connectionFactory, options.Resilience); var healthService = new DatabaseHealthService(executor, options); return new DatabaseRuntime(options, connectionFactory, executor, healthService); } }