980 lines
32 KiB
C#
980 lines
32 KiB
C#
using System.Text.Json;
|
|
|
|
namespace MBN_STOCK_WEBVIEW.LegacyApplication;
|
|
|
|
public sealed record LegacyOperatorSettings(
|
|
string? SceneDirectory,
|
|
string? ResourceDirectory,
|
|
string? BackgroundDirectory,
|
|
bool NavigationExpanded)
|
|
{
|
|
public const int CurrentSchemaVersion = 1;
|
|
|
|
public static LegacyOperatorSettings Default { get; } = new(
|
|
SceneDirectory: null,
|
|
ResourceDirectory: null,
|
|
BackgroundDirectory: null,
|
|
NavigationExpanded: true);
|
|
}
|
|
|
|
public enum LegacyOperatorSettingsFolderKind
|
|
{
|
|
Scene,
|
|
Resource,
|
|
Background
|
|
}
|
|
|
|
public enum LegacyOperatorFolderValidationFailure
|
|
{
|
|
None,
|
|
InvalidPath,
|
|
DirectoryNotFound,
|
|
RootDirectory,
|
|
NonFixedDrive,
|
|
ReparsePoint,
|
|
RequiredFileMissing,
|
|
RequiredFileUnsafe,
|
|
InvalidRuntimeCatalog,
|
|
FileUnavailable
|
|
}
|
|
|
|
public sealed record LegacyOperatorValidatedFolder(
|
|
string CanonicalPath,
|
|
string AbbreviatedPath,
|
|
bool DatabaseIniDetected);
|
|
|
|
public sealed record LegacyOperatorFolderValidationResult(
|
|
LegacyOperatorValidatedFolder? Folder,
|
|
LegacyOperatorFolderValidationFailure Failure,
|
|
string? WarningMessage)
|
|
{
|
|
public bool IsValid => Failure == LegacyOperatorFolderValidationFailure.None &&
|
|
Folder is not null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates only native folder-picker results. The returned messages intentionally
|
|
/// contain no source path; a separately bounded abbreviation is available for display.
|
|
/// </summary>
|
|
public static class LegacyOperatorFolderValidator
|
|
{
|
|
public const int MaximumPathCharacters = 1024;
|
|
public const int DefaultDisplayCharacters = 48;
|
|
public const string UnconfiguredDisplay = "선택되지 않음";
|
|
|
|
private const string InvalidFolderMessage =
|
|
"선택한 폴더를 안전하게 사용할 수 없습니다.";
|
|
private const string MissingSceneMessage =
|
|
"선택한 디자인 폴더에 필수 장면 파일이 없습니다.";
|
|
private const string InvalidResourceMessage =
|
|
"선택한 설정 폴더의 UI 설정 파일을 검증할 수 없습니다.";
|
|
|
|
private static readonly string[] ResourceFileNames =
|
|
[
|
|
"종목.ini",
|
|
"업종_코스피.ini",
|
|
"업종_코스닥.ini",
|
|
"해외.ini",
|
|
"환율.ini",
|
|
"지수.ini",
|
|
"종목비교.ini"
|
|
];
|
|
|
|
public static bool TryValidate(
|
|
LegacyOperatorSettingsFolderKind kind,
|
|
string? path,
|
|
out LegacyOperatorValidatedFolder? folder,
|
|
out string? warningMessage)
|
|
{
|
|
var result = Validate(kind, path);
|
|
folder = result.Folder;
|
|
warningMessage = result.WarningMessage;
|
|
return result.IsValid;
|
|
}
|
|
|
|
public static LegacyOperatorFolderValidationResult Validate(
|
|
LegacyOperatorSettingsFolderKind kind,
|
|
string? path)
|
|
{
|
|
var failure = LegacyOperatorFolderValidationFailure.InvalidPath;
|
|
if (!Enum.IsDefined(kind) || !TryCanonicalizeDirectory(
|
|
path,
|
|
out var canonicalPath,
|
|
out failure))
|
|
{
|
|
return Invalid(failure, InvalidFolderMessage);
|
|
}
|
|
|
|
var databaseIniDetected = false;
|
|
switch (kind)
|
|
{
|
|
case LegacyOperatorSettingsFolderKind.Scene:
|
|
if (!TryValidateRegularFile(
|
|
Path.Combine(canonicalPath, "5001.t2s"),
|
|
required: true,
|
|
out failure))
|
|
{
|
|
return Invalid(failure, MissingSceneMessage);
|
|
}
|
|
|
|
break;
|
|
|
|
case LegacyOperatorSettingsFolderKind.Resource:
|
|
foreach (var fileName in ResourceFileNames)
|
|
{
|
|
if (!TryValidateRegularFile(
|
|
Path.Combine(canonicalPath, fileName),
|
|
required: true,
|
|
out failure))
|
|
{
|
|
return Invalid(failure, InvalidResourceMessage);
|
|
}
|
|
}
|
|
|
|
var stockMenu = LegacyRuntimeStockCutMenuLoader
|
|
.ResolveFromResourceDirectory(canonicalPath);
|
|
if (stockMenu.UsesBuiltInCatalog ||
|
|
stockMenu.Failure != LegacyRuntimeStockCutMenuFailure.None)
|
|
{
|
|
return Invalid(
|
|
LegacyOperatorFolderValidationFailure.InvalidRuntimeCatalog,
|
|
InvalidResourceMessage);
|
|
}
|
|
|
|
var uiCatalogs = LegacyRuntimeUiCatalogLoader
|
|
.ResolveFromResourceDirectory(canonicalPath);
|
|
if (uiCatalogs.Warnings.Count != 0)
|
|
{
|
|
return Invalid(
|
|
LegacyOperatorFolderValidationFailure.InvalidRuntimeCatalog,
|
|
InvalidResourceMessage);
|
|
}
|
|
|
|
var databaseIniPath = Path.Combine(canonicalPath, "MmoneyCoder.ini");
|
|
if (!TryInspectOptionalRegularFile(
|
|
databaseIniPath,
|
|
out databaseIniDetected,
|
|
out failure))
|
|
{
|
|
return Invalid(failure, InvalidResourceMessage);
|
|
}
|
|
|
|
break;
|
|
|
|
case LegacyOperatorSettingsFolderKind.Background:
|
|
break;
|
|
|
|
default:
|
|
return Invalid(
|
|
LegacyOperatorFolderValidationFailure.InvalidPath,
|
|
InvalidFolderMessage);
|
|
}
|
|
|
|
return new LegacyOperatorFolderValidationResult(
|
|
new LegacyOperatorValidatedFolder(
|
|
canonicalPath,
|
|
AbbreviateForDisplay(canonicalPath),
|
|
databaseIniDetected),
|
|
LegacyOperatorFolderValidationFailure.None,
|
|
WarningMessage: null);
|
|
}
|
|
|
|
public static string AbbreviateForDisplay(
|
|
string? path,
|
|
int maximumCharacters = DefaultDisplayCharacters)
|
|
{
|
|
maximumCharacters = Math.Clamp(maximumCharacters, 12, 120);
|
|
if (string.IsNullOrWhiteSpace(path) ||
|
|
path.Any(char.IsControl) ||
|
|
!Path.IsPathFullyQualified(path))
|
|
{
|
|
return UnconfiguredDisplay;
|
|
}
|
|
|
|
try
|
|
{
|
|
var canonicalPath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(path));
|
|
var root = Path.GetPathRoot(canonicalPath);
|
|
var leaf = Path.GetFileName(canonicalPath);
|
|
if (string.IsNullOrEmpty(root) || string.IsNullOrEmpty(leaf))
|
|
{
|
|
return UnconfiguredDisplay;
|
|
}
|
|
|
|
var display = string.Concat(
|
|
Path.TrimEndingDirectorySeparator(root),
|
|
Path.DirectorySeparatorChar,
|
|
"…",
|
|
Path.DirectorySeparatorChar,
|
|
leaf);
|
|
if (display.Length <= maximumCharacters)
|
|
{
|
|
return display;
|
|
}
|
|
|
|
var prefix = string.Concat(
|
|
Path.TrimEndingDirectorySeparator(root),
|
|
Path.DirectorySeparatorChar,
|
|
"…",
|
|
Path.DirectorySeparatorChar);
|
|
var availableLeafCharacters = Math.Max(
|
|
1,
|
|
maximumCharacters - prefix.Length - 1);
|
|
return string.Concat(
|
|
prefix,
|
|
"…",
|
|
leaf[^Math.Min(availableLeafCharacters, leaf.Length)..]);
|
|
}
|
|
catch (Exception exception) when (
|
|
exception is ArgumentException or IOException or NotSupportedException or
|
|
System.Security.SecurityException)
|
|
{
|
|
return UnconfiguredDisplay;
|
|
}
|
|
}
|
|
|
|
internal static bool TryCanonicalizeDirectory(
|
|
string? path,
|
|
out string canonicalPath,
|
|
out LegacyOperatorFolderValidationFailure failure)
|
|
{
|
|
canonicalPath = string.Empty;
|
|
if (string.IsNullOrWhiteSpace(path) ||
|
|
path.Length > MaximumPathCharacters ||
|
|
path.Any(char.IsControl) ||
|
|
!Path.IsPathFullyQualified(path))
|
|
{
|
|
failure = LegacyOperatorFolderValidationFailure.InvalidPath;
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
canonicalPath = Path.TrimEndingDirectorySeparator(Path.GetFullPath(path));
|
|
var root = Path.GetPathRoot(canonicalPath);
|
|
if (string.IsNullOrEmpty(root) ||
|
|
root.StartsWith("\\\\", StringComparison.Ordinal) ||
|
|
!string.Equals(
|
|
Path.GetPathRoot(root),
|
|
root,
|
|
StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
failure = LegacyOperatorFolderValidationFailure.InvalidPath;
|
|
canonicalPath = string.Empty;
|
|
return false;
|
|
}
|
|
|
|
if (string.Equals(
|
|
Path.TrimEndingDirectorySeparator(root),
|
|
canonicalPath,
|
|
StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
failure = LegacyOperatorFolderValidationFailure.RootDirectory;
|
|
canonicalPath = string.Empty;
|
|
return false;
|
|
}
|
|
|
|
var drive = new DriveInfo(root);
|
|
if (!drive.IsReady || drive.DriveType != DriveType.Fixed)
|
|
{
|
|
failure = LegacyOperatorFolderValidationFailure.NonFixedDrive;
|
|
canonicalPath = string.Empty;
|
|
return false;
|
|
}
|
|
|
|
if (!Directory.Exists(canonicalPath))
|
|
{
|
|
failure = LegacyOperatorFolderValidationFailure.DirectoryNotFound;
|
|
canonicalPath = string.Empty;
|
|
return false;
|
|
}
|
|
|
|
for (DirectoryInfo? current = new(canonicalPath);
|
|
current is not null;
|
|
current = current.Parent)
|
|
{
|
|
if ((current.Attributes & FileAttributes.ReparsePoint) != 0)
|
|
{
|
|
failure = LegacyOperatorFolderValidationFailure.ReparsePoint;
|
|
canonicalPath = string.Empty;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
failure = LegacyOperatorFolderValidationFailure.None;
|
|
return true;
|
|
}
|
|
catch (Exception exception) when (
|
|
exception is ArgumentException or IOException or UnauthorizedAccessException or
|
|
NotSupportedException or System.Security.SecurityException)
|
|
{
|
|
failure = LegacyOperatorFolderValidationFailure.FileUnavailable;
|
|
canonicalPath = string.Empty;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool TryValidateRegularFile(
|
|
string path,
|
|
bool required,
|
|
out LegacyOperatorFolderValidationFailure failure)
|
|
{
|
|
try
|
|
{
|
|
if (!File.Exists(path))
|
|
{
|
|
failure = required
|
|
? LegacyOperatorFolderValidationFailure.RequiredFileMissing
|
|
: LegacyOperatorFolderValidationFailure.None;
|
|
return !required;
|
|
}
|
|
|
|
var attributes = File.GetAttributes(path);
|
|
if ((attributes & (FileAttributes.Directory |
|
|
FileAttributes.ReparsePoint |
|
|
FileAttributes.Device)) != 0)
|
|
{
|
|
failure = LegacyOperatorFolderValidationFailure.RequiredFileUnsafe;
|
|
return false;
|
|
}
|
|
|
|
failure = LegacyOperatorFolderValidationFailure.None;
|
|
return true;
|
|
}
|
|
catch (Exception exception) when (
|
|
exception is ArgumentException or IOException or UnauthorizedAccessException or
|
|
NotSupportedException or System.Security.SecurityException)
|
|
{
|
|
failure = LegacyOperatorFolderValidationFailure.FileUnavailable;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool TryInspectOptionalRegularFile(
|
|
string path,
|
|
out bool detected,
|
|
out LegacyOperatorFolderValidationFailure failure)
|
|
{
|
|
detected = false;
|
|
try
|
|
{
|
|
var attributes = File.GetAttributes(path);
|
|
if ((attributes & (FileAttributes.Directory |
|
|
FileAttributes.ReparsePoint |
|
|
FileAttributes.Device)) != 0)
|
|
{
|
|
failure = LegacyOperatorFolderValidationFailure.RequiredFileUnsafe;
|
|
return false;
|
|
}
|
|
|
|
detected = true;
|
|
failure = LegacyOperatorFolderValidationFailure.None;
|
|
return true;
|
|
}
|
|
catch (Exception exception) when (
|
|
exception is FileNotFoundException or DirectoryNotFoundException)
|
|
{
|
|
failure = LegacyOperatorFolderValidationFailure.None;
|
|
return true;
|
|
}
|
|
catch (Exception exception) when (
|
|
exception is ArgumentException or IOException or UnauthorizedAccessException or
|
|
NotSupportedException or System.Security.SecurityException)
|
|
{
|
|
failure = LegacyOperatorFolderValidationFailure.FileUnavailable;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static LegacyOperatorFolderValidationResult Invalid(
|
|
LegacyOperatorFolderValidationFailure failure,
|
|
string warningMessage) => new(
|
|
Folder: null,
|
|
failure,
|
|
warningMessage);
|
|
}
|
|
|
|
public enum LegacyOperatorSettingsStoreFailure
|
|
{
|
|
None,
|
|
InvalidConfigurationPath,
|
|
FileUnavailable,
|
|
FileTooLarge,
|
|
InvalidJson,
|
|
UnsupportedSchema,
|
|
InvalidSettings,
|
|
SaveUnavailable
|
|
}
|
|
|
|
public sealed record LegacyOperatorSettingsLoadResult(
|
|
LegacyOperatorSettings Settings,
|
|
string? WarningMessage)
|
|
{
|
|
public LegacyOperatorSettingsStoreFailure Failure { get; init; }
|
|
|
|
public bool LoadedFromDisk { get; init; }
|
|
}
|
|
|
|
public sealed record LegacyOperatorSettingsSaveResult(
|
|
bool Succeeded,
|
|
string? WarningMessage)
|
|
{
|
|
public LegacyOperatorSettingsStoreFailure Failure { get; init; }
|
|
|
|
public LegacyOperatorSettings? SavedSettings { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stores only native-approved runtime folders and one presentation preference.
|
|
/// Credentials, playout targets, live authorization and asset contents never enter
|
|
/// this file.
|
|
/// </summary>
|
|
public sealed class LegacyOperatorSettingsStore
|
|
{
|
|
public const int MaximumFileBytes = 16 * 1024;
|
|
public const string LoadWarningMessage =
|
|
"저장된 폴더 설정을 읽지 못해 안전한 기본값을 사용합니다.";
|
|
public const string SaveWarningMessage =
|
|
"폴더 설정을 안전하게 저장하지 못했습니다.";
|
|
|
|
private const string FileName = "runtime-folders.local.json";
|
|
private readonly string _configurationPath;
|
|
|
|
public LegacyOperatorSettingsStore(string? configurationPath = null)
|
|
{
|
|
_configurationPath = configurationPath ?? DefaultPath;
|
|
}
|
|
|
|
public static string DefaultPath => Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
"MBN_STOCK_WEBVIEW",
|
|
"Config",
|
|
FileName);
|
|
|
|
public string ConfigurationPath => _configurationPath;
|
|
|
|
public LegacyOperatorSettingsLoadResult Load()
|
|
{
|
|
if (!TryNormalizeConfigurationPath(_configurationPath, out var path))
|
|
{
|
|
return FailedLoad(LegacyOperatorSettingsStoreFailure.InvalidConfigurationPath);
|
|
}
|
|
|
|
try
|
|
{
|
|
var directory = Path.GetDirectoryName(path);
|
|
if (string.IsNullOrEmpty(directory))
|
|
{
|
|
return FailedLoad(
|
|
LegacyOperatorSettingsStoreFailure.InvalidConfigurationPath);
|
|
}
|
|
|
|
if (!Directory.Exists(directory))
|
|
{
|
|
return new LegacyOperatorSettingsLoadResult(
|
|
LegacyOperatorSettings.Default,
|
|
WarningMessage: null)
|
|
{
|
|
Failure = LegacyOperatorSettingsStoreFailure.None,
|
|
LoadedFromDisk = false
|
|
};
|
|
}
|
|
|
|
if (!LegacyOperatorFolderValidator.TryCanonicalizeDirectory(
|
|
directory,
|
|
out var canonicalDirectory,
|
|
out _))
|
|
{
|
|
return FailedLoad(
|
|
LegacyOperatorSettingsStoreFailure.InvalidConfigurationPath);
|
|
}
|
|
|
|
path = Path.Combine(canonicalDirectory, Path.GetFileName(path));
|
|
if (!File.Exists(path))
|
|
{
|
|
return new LegacyOperatorSettingsLoadResult(
|
|
LegacyOperatorSettings.Default,
|
|
WarningMessage: null)
|
|
{
|
|
Failure = LegacyOperatorSettingsStoreFailure.None,
|
|
LoadedFromDisk = false
|
|
};
|
|
}
|
|
|
|
var attributes = File.GetAttributes(path);
|
|
if ((attributes & (FileAttributes.Directory |
|
|
FileAttributes.ReparsePoint |
|
|
FileAttributes.Device)) != 0)
|
|
{
|
|
return FailedLoad(LegacyOperatorSettingsStoreFailure.FileUnavailable);
|
|
}
|
|
|
|
using var stream = new FileStream(
|
|
path,
|
|
FileMode.Open,
|
|
FileAccess.Read,
|
|
FileShare.Read,
|
|
bufferSize: 4096,
|
|
FileOptions.SequentialScan);
|
|
if (stream.Length is <= 0 or > MaximumFileBytes)
|
|
{
|
|
return FailedLoad(
|
|
stream.Length > MaximumFileBytes
|
|
? LegacyOperatorSettingsStoreFailure.FileTooLarge
|
|
: LegacyOperatorSettingsStoreFailure.InvalidJson);
|
|
}
|
|
|
|
var json = new byte[(int)stream.Length];
|
|
stream.ReadExactly(json);
|
|
if (!TryDeserialize(
|
|
json,
|
|
out var settings,
|
|
out var failure) ||
|
|
!TryNormalizeSettings(settings, out var normalizedSettings))
|
|
{
|
|
return FailedLoad(
|
|
failure == LegacyOperatorSettingsStoreFailure.None
|
|
? LegacyOperatorSettingsStoreFailure.InvalidSettings
|
|
: failure);
|
|
}
|
|
|
|
return new LegacyOperatorSettingsLoadResult(
|
|
normalizedSettings,
|
|
WarningMessage: null)
|
|
{
|
|
Failure = LegacyOperatorSettingsStoreFailure.None,
|
|
LoadedFromDisk = true
|
|
};
|
|
}
|
|
catch (Exception exception) when (
|
|
exception is ArgumentException or IOException or UnauthorizedAccessException or
|
|
NotSupportedException or System.Security.SecurityException)
|
|
{
|
|
return FailedLoad(LegacyOperatorSettingsStoreFailure.FileUnavailable);
|
|
}
|
|
}
|
|
|
|
public LegacyOperatorSettingsSaveResult Save(LegacyOperatorSettings settings)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(settings);
|
|
if (!TryNormalizeSettings(settings, out var normalizedSettings))
|
|
{
|
|
return FailedSave(LegacyOperatorSettingsStoreFailure.InvalidSettings);
|
|
}
|
|
|
|
if (!TryNormalizeConfigurationPath(_configurationPath, out var path))
|
|
{
|
|
return FailedSave(
|
|
LegacyOperatorSettingsStoreFailure.InvalidConfigurationPath);
|
|
}
|
|
|
|
string? temporaryPath = null;
|
|
try
|
|
{
|
|
var directory = Path.GetDirectoryName(path);
|
|
if (string.IsNullOrEmpty(directory))
|
|
{
|
|
return FailedSave(
|
|
LegacyOperatorSettingsStoreFailure.InvalidConfigurationPath);
|
|
}
|
|
|
|
if (!TryCreateSafeConfigurationDirectory(
|
|
directory,
|
|
out var canonicalDirectory))
|
|
{
|
|
return FailedSave(
|
|
LegacyOperatorSettingsStoreFailure.InvalidConfigurationPath);
|
|
}
|
|
|
|
path = Path.Combine(canonicalDirectory, Path.GetFileName(path));
|
|
if (File.Exists(path))
|
|
{
|
|
var attributes = File.GetAttributes(path);
|
|
if ((attributes & (FileAttributes.Directory |
|
|
FileAttributes.ReparsePoint |
|
|
FileAttributes.Device)) != 0)
|
|
{
|
|
return FailedSave(
|
|
LegacyOperatorSettingsStoreFailure.SaveUnavailable);
|
|
}
|
|
}
|
|
|
|
var json = Serialize(normalizedSettings);
|
|
if (json.Length > MaximumFileBytes)
|
|
{
|
|
return FailedSave(LegacyOperatorSettingsStoreFailure.FileTooLarge);
|
|
}
|
|
|
|
temporaryPath = Path.Combine(
|
|
canonicalDirectory,
|
|
$".{FileName}.{Guid.NewGuid():N}.tmp");
|
|
using (var stream = new FileStream(
|
|
temporaryPath,
|
|
FileMode.CreateNew,
|
|
FileAccess.Write,
|
|
FileShare.None,
|
|
bufferSize: 4096,
|
|
FileOptions.WriteThrough))
|
|
{
|
|
stream.Write(json);
|
|
stream.Flush(flushToDisk: true);
|
|
}
|
|
|
|
File.Move(temporaryPath, path, overwrite: true);
|
|
temporaryPath = null;
|
|
return new LegacyOperatorSettingsSaveResult(
|
|
Succeeded: true,
|
|
WarningMessage: null)
|
|
{
|
|
Failure = LegacyOperatorSettingsStoreFailure.None,
|
|
SavedSettings = normalizedSettings
|
|
};
|
|
}
|
|
catch (Exception exception) when (
|
|
exception is ArgumentException or IOException or UnauthorizedAccessException or
|
|
NotSupportedException or System.Security.SecurityException)
|
|
{
|
|
return FailedSave(LegacyOperatorSettingsStoreFailure.SaveUnavailable);
|
|
}
|
|
finally
|
|
{
|
|
if (temporaryPath is not null)
|
|
{
|
|
try
|
|
{
|
|
File.Delete(temporaryPath);
|
|
}
|
|
catch (Exception exception) when (
|
|
exception is IOException or UnauthorizedAccessException or
|
|
NotSupportedException or System.Security.SecurityException)
|
|
{
|
|
// A failed save remains failed. Never replace that result with a
|
|
// cleanup exception or expose the temporary path.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool TryNormalizeSettings(
|
|
LegacyOperatorSettings settings,
|
|
out LegacyOperatorSettings normalizedSettings)
|
|
{
|
|
normalizedSettings = LegacyOperatorSettings.Default;
|
|
if (!TryNormalizeOptionalFolder(
|
|
LegacyOperatorSettingsFolderKind.Scene,
|
|
settings.SceneDirectory,
|
|
out var sceneDirectory) ||
|
|
!TryNormalizeOptionalFolder(
|
|
LegacyOperatorSettingsFolderKind.Resource,
|
|
settings.ResourceDirectory,
|
|
out var resourceDirectory) ||
|
|
!TryNormalizeOptionalFolder(
|
|
LegacyOperatorSettingsFolderKind.Background,
|
|
settings.BackgroundDirectory,
|
|
out var backgroundDirectory))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
normalizedSettings = new LegacyOperatorSettings(
|
|
sceneDirectory,
|
|
resourceDirectory,
|
|
backgroundDirectory,
|
|
settings.NavigationExpanded);
|
|
return true;
|
|
}
|
|
|
|
private static bool TryNormalizeOptionalFolder(
|
|
LegacyOperatorSettingsFolderKind kind,
|
|
string? path,
|
|
out string? canonicalPath)
|
|
{
|
|
canonicalPath = null;
|
|
if (path is null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!LegacyOperatorFolderValidator.TryValidate(
|
|
kind,
|
|
path,
|
|
out var folder,
|
|
out _))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
canonicalPath = folder!.CanonicalPath;
|
|
return true;
|
|
}
|
|
|
|
private static byte[] Serialize(LegacyOperatorSettings settings)
|
|
{
|
|
using var stream = new MemoryStream(capacity: 512);
|
|
using (var writer = new Utf8JsonWriter(
|
|
stream,
|
|
new JsonWriterOptions { Indented = true }))
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteNumber("schemaVersion", LegacyOperatorSettings.CurrentSchemaVersion);
|
|
WriteOptionalString(writer, "sceneDirectory", settings.SceneDirectory);
|
|
WriteOptionalString(writer, "resourceDirectory", settings.ResourceDirectory);
|
|
WriteOptionalString(writer, "backgroundDirectory", settings.BackgroundDirectory);
|
|
writer.WriteBoolean("navigationExpanded", settings.NavigationExpanded);
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
return stream.ToArray();
|
|
}
|
|
|
|
private static void WriteOptionalString(
|
|
Utf8JsonWriter writer,
|
|
string name,
|
|
string? value)
|
|
{
|
|
if (value is null)
|
|
{
|
|
writer.WriteNull(name);
|
|
}
|
|
else
|
|
{
|
|
writer.WriteString(name, value);
|
|
}
|
|
}
|
|
|
|
private static bool TryDeserialize(
|
|
ReadOnlyMemory<byte> json,
|
|
out LegacyOperatorSettings settings,
|
|
out LegacyOperatorSettingsStoreFailure failure)
|
|
{
|
|
settings = LegacyOperatorSettings.Default;
|
|
failure = LegacyOperatorSettingsStoreFailure.InvalidJson;
|
|
try
|
|
{
|
|
using var document = JsonDocument.Parse(
|
|
json,
|
|
new JsonDocumentOptions
|
|
{
|
|
AllowTrailingCommas = false,
|
|
CommentHandling = JsonCommentHandling.Disallow,
|
|
MaxDepth = 4
|
|
});
|
|
if (document.RootElement.ValueKind != JsonValueKind.Object)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var properties = new Dictionary<string, JsonElement>(StringComparer.Ordinal);
|
|
foreach (var property in document.RootElement.EnumerateObject())
|
|
{
|
|
if (!IsExpectedProperty(property.Name) ||
|
|
!properties.TryAdd(property.Name, property.Value))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (properties.Count != 5 ||
|
|
!TryReadInt32(properties, "schemaVersion", out var schemaVersion) ||
|
|
!TryReadOptionalString(properties, "sceneDirectory", out var sceneDirectory) ||
|
|
!TryReadOptionalString(
|
|
properties,
|
|
"resourceDirectory",
|
|
out var resourceDirectory) ||
|
|
!TryReadOptionalString(
|
|
properties,
|
|
"backgroundDirectory",
|
|
out var backgroundDirectory) ||
|
|
!TryReadBoolean(
|
|
properties,
|
|
"navigationExpanded",
|
|
out var navigationExpanded))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (schemaVersion != LegacyOperatorSettings.CurrentSchemaVersion)
|
|
{
|
|
failure = LegacyOperatorSettingsStoreFailure.UnsupportedSchema;
|
|
return false;
|
|
}
|
|
|
|
settings = new LegacyOperatorSettings(
|
|
sceneDirectory,
|
|
resourceDirectory,
|
|
backgroundDirectory,
|
|
navigationExpanded);
|
|
failure = LegacyOperatorSettingsStoreFailure.None;
|
|
return true;
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
return false;
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
return false;
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
return false;
|
|
}
|
|
catch (OverflowException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool IsExpectedProperty(string name) => name is
|
|
"schemaVersion" or
|
|
"sceneDirectory" or
|
|
"resourceDirectory" or
|
|
"backgroundDirectory" or
|
|
"navigationExpanded";
|
|
|
|
private static bool TryReadInt32(
|
|
IReadOnlyDictionary<string, JsonElement> properties,
|
|
string name,
|
|
out int value)
|
|
{
|
|
value = default;
|
|
return properties.TryGetValue(name, out var element) &&
|
|
element.ValueKind == JsonValueKind.Number &&
|
|
element.TryGetInt32(out value);
|
|
}
|
|
|
|
private static bool TryReadBoolean(
|
|
IReadOnlyDictionary<string, JsonElement> properties,
|
|
string name,
|
|
out bool value)
|
|
{
|
|
value = default;
|
|
if (!properties.TryGetValue(name, out var element) ||
|
|
element.ValueKind is not (JsonValueKind.True or JsonValueKind.False))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
value = element.GetBoolean();
|
|
return true;
|
|
}
|
|
|
|
private static bool TryReadOptionalString(
|
|
IReadOnlyDictionary<string, JsonElement> properties,
|
|
string name,
|
|
out string? value)
|
|
{
|
|
value = null;
|
|
if (!properties.TryGetValue(name, out var element))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (element.ValueKind == JsonValueKind.Null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (element.ValueKind != JsonValueKind.String)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
value = element.GetString();
|
|
return value is not null;
|
|
}
|
|
|
|
private static bool TryNormalizeConfigurationPath(
|
|
string? path,
|
|
out string normalizedPath)
|
|
{
|
|
normalizedPath = string.Empty;
|
|
if (string.IsNullOrWhiteSpace(path) ||
|
|
path.Length > LegacyOperatorFolderValidator.MaximumPathCharacters ||
|
|
path.Any(char.IsControl) ||
|
|
!Path.IsPathFullyQualified(path))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
normalizedPath = Path.GetFullPath(path);
|
|
var root = Path.GetPathRoot(normalizedPath);
|
|
var fileName = Path.GetFileName(normalizedPath);
|
|
return !string.IsNullOrEmpty(root) &&
|
|
!root.StartsWith("\\\\", StringComparison.Ordinal) &&
|
|
!string.IsNullOrWhiteSpace(fileName) &&
|
|
!fileName.Any(char.IsControl);
|
|
}
|
|
catch (Exception exception) when (
|
|
exception is ArgumentException or IOException or NotSupportedException or
|
|
System.Security.SecurityException)
|
|
{
|
|
normalizedPath = string.Empty;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool TryCreateSafeConfigurationDirectory(
|
|
string directory,
|
|
out string canonicalDirectory)
|
|
{
|
|
canonicalDirectory = string.Empty;
|
|
try
|
|
{
|
|
var existingAncestor = directory;
|
|
while (!Directory.Exists(existingAncestor))
|
|
{
|
|
var parent = Path.GetDirectoryName(existingAncestor);
|
|
if (string.IsNullOrEmpty(parent) ||
|
|
string.Equals(parent, existingAncestor, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
existingAncestor = parent;
|
|
}
|
|
|
|
if (!LegacyOperatorFolderValidator.TryCanonicalizeDirectory(
|
|
existingAncestor,
|
|
out _,
|
|
out _))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Directory.CreateDirectory(directory);
|
|
return LegacyOperatorFolderValidator.TryCanonicalizeDirectory(
|
|
directory,
|
|
out canonicalDirectory,
|
|
out _);
|
|
}
|
|
catch (Exception exception) when (
|
|
exception is ArgumentException or IOException or UnauthorizedAccessException or
|
|
NotSupportedException or System.Security.SecurityException)
|
|
{
|
|
canonicalDirectory = string.Empty;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static LegacyOperatorSettingsLoadResult FailedLoad(
|
|
LegacyOperatorSettingsStoreFailure failure) => new(
|
|
LegacyOperatorSettings.Default,
|
|
LoadWarningMessage)
|
|
{
|
|
Failure = failure,
|
|
LoadedFromDisk = false
|
|
};
|
|
|
|
private static LegacyOperatorSettingsSaveResult FailedSave(
|
|
LegacyOperatorSettingsStoreFailure failure) => new(
|
|
Succeeded: false,
|
|
SaveWarningMessage)
|
|
{
|
|
Failure = failure,
|
|
SavedSettings = null
|
|
};
|
|
}
|