feat: load legacy Cuts and Res runtime assets
This commit is contained in:
@@ -111,6 +111,8 @@ internal static class LegacyComparisonWorkflowCatalog
|
||||
|
||||
public static IReadOnlyList<LegacyComparisonTarget> FixedTargets => FixedTargetList;
|
||||
|
||||
internal static IReadOnlyList<LegacyComparisonActionDefinition> Actions => ActionList;
|
||||
|
||||
public static LegacyComparisonTarget? GetFixedTarget(string targetId) =>
|
||||
targetId is not null && FixedById.TryGetValue(targetId, out var target) ? target : null;
|
||||
|
||||
@@ -195,13 +197,31 @@ internal static class LegacyComparisonWorkflowCatalog
|
||||
LegacyComparisonTarget? first,
|
||||
LegacyComparisonTarget? second)
|
||||
{
|
||||
var views = ActionList.Select(action =>
|
||||
var placements = ActionList
|
||||
.Select(action => new LegacyComparisonActionPlacement(action.ActionId, action.Section))
|
||||
.ToArray();
|
||||
return CreateActionViews(first, second, placements);
|
||||
}
|
||||
|
||||
internal static IReadOnlyList<LegacyComparisonActionView> CreateActionViews(
|
||||
LegacyComparisonTarget? first,
|
||||
LegacyComparisonTarget? second,
|
||||
IReadOnlyList<LegacyComparisonActionPlacement> placements)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(placements);
|
||||
var views = placements.Select(placement =>
|
||||
{
|
||||
if (!ActionsById.TryGetValue(placement.ActionId, out var action))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The runtime comparison layout contains an unknown action.");
|
||||
}
|
||||
|
||||
var compatibility = GetCompatibility(action, first, second);
|
||||
return new LegacyComparisonActionView(
|
||||
action.ActionId,
|
||||
action.Label,
|
||||
action.Section,
|
||||
placement.Section,
|
||||
compatibility.IsAvailable,
|
||||
compatibility.Reason);
|
||||
}).ToArray();
|
||||
|
||||
@@ -11,6 +11,7 @@ public sealed class LegacyComparisonWorkflowController
|
||||
{
|
||||
private readonly ILegacyComparisonDataService _dataService;
|
||||
private readonly ILegacyComparisonPairStore _pairStore;
|
||||
private readonly LegacyComparisonUiLayout _uiLayout;
|
||||
private List<SearchResultState> _domesticResults = [];
|
||||
private List<SearchResultState> _worldResults = [];
|
||||
private List<SavedPairState> _savedPairs = [];
|
||||
@@ -38,10 +39,12 @@ public sealed class LegacyComparisonWorkflowController
|
||||
|
||||
public LegacyComparisonWorkflowController(
|
||||
ILegacyComparisonDataService dataService,
|
||||
ILegacyComparisonPairStore pairStore)
|
||||
ILegacyComparisonPairStore pairStore,
|
||||
LegacyComparisonUiLayout? uiLayout = null)
|
||||
{
|
||||
_dataService = dataService ?? throw new ArgumentNullException(nameof(dataService));
|
||||
_pairStore = pairStore ?? throw new ArgumentNullException(nameof(pairStore));
|
||||
_uiLayout = uiLayout ?? LegacyComparisonUiLayout.Default;
|
||||
}
|
||||
|
||||
public LegacyComparisonWorkflowSnapshot Current => CreateSnapshot();
|
||||
@@ -606,7 +609,7 @@ public sealed class LegacyComparisonWorkflowController
|
||||
result.Target,
|
||||
result.SelectionId))
|
||||
.ToArray();
|
||||
var fixedTargets = LegacyComparisonWorkflowCatalog.FixedTargets
|
||||
var fixedTargets = _uiLayout.FixedTargets
|
||||
.Select(target => LegacyComparisonWorkflowCatalog.ToView(target, target.TargetId))
|
||||
.ToArray();
|
||||
var savedPairs = _savedPairs.Select((pair, index) =>
|
||||
@@ -645,7 +648,7 @@ public sealed class LegacyComparisonWorkflowController
|
||||
: LegacyComparisonWorkflowCatalog.ToView(_secondTarget, "comparison-draft-second"),
|
||||
Array.AsReadOnly(savedPairs),
|
||||
_selectedPairId,
|
||||
LegacyComparisonWorkflowCatalog.CreateActionViews(
|
||||
_uiLayout.CreateActionViews(
|
||||
selectedPair?.First,
|
||||
selectedPair?.Second),
|
||||
_statusMessage,
|
||||
|
||||
@@ -259,6 +259,35 @@ public sealed class LegacyFixedActionCatalog
|
||||
return RuntimeAssets[market];
|
||||
}
|
||||
|
||||
internal LegacyFixedActionCatalog WithRuntimeOrder(
|
||||
IReadOnlyDictionary<LegacyFixedMarket, IReadOnlyList<LegacyFixedAction>> actionsByMarket)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(actionsByMarket);
|
||||
var ordered = new List<LegacyFixedAction>(Actions.Count);
|
||||
foreach (var market in Enum.GetValues<LegacyFixedMarket>())
|
||||
{
|
||||
var source = actionsByMarket.TryGetValue(market, out var runtimeActions)
|
||||
? runtimeActions
|
||||
: _byMarket[market];
|
||||
if (source is null || source.Count != _byMarket[market].Count ||
|
||||
source.Any(action => action is null || action.Market != market) ||
|
||||
!source.Select(action => action.Id).ToHashSet(StringComparer.Ordinal)
|
||||
.SetEquals(_byMarket[market].Select(action => action.Id)))
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"The runtime fixed-action order for '{MarketText(market)}' is invalid.");
|
||||
}
|
||||
|
||||
ordered.AddRange(source);
|
||||
}
|
||||
|
||||
return new LegacyFixedActionCatalog(
|
||||
ReadOnly(ordered),
|
||||
RuntimeAssets,
|
||||
DocumentSha256,
|
||||
GeneratorSourceSha256);
|
||||
}
|
||||
|
||||
public LegacyMaterializedFixedAction Materialize(
|
||||
string actionId,
|
||||
DateTimeOffset localNow)
|
||||
|
||||
@@ -70,6 +70,7 @@ public sealed class LegacyIndustrySelectionWorkflow
|
||||
|
||||
private readonly IIndustrySelectionService _selectionService;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly LegacyIndustryActionLayout _actionLayout;
|
||||
private IReadOnlyList<IndustrySelection> _industries = NoIndustries;
|
||||
private IReadOnlyList<LegacyIndustryActionDefinition> _actions = NoActions;
|
||||
private IndustryMarket? _market;
|
||||
@@ -84,11 +85,13 @@ public sealed class LegacyIndustrySelectionWorkflow
|
||||
|
||||
public LegacyIndustrySelectionWorkflow(
|
||||
IIndustrySelectionService selectionService,
|
||||
TimeProvider? timeProvider = null)
|
||||
TimeProvider? timeProvider = null,
|
||||
LegacyIndustryActionLayout? actionLayout = null)
|
||||
{
|
||||
_selectionService = selectionService ??
|
||||
throw new ArgumentNullException(nameof(selectionService));
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_actionLayout = actionLayout ?? LegacyIndustryActionLayout.Default;
|
||||
}
|
||||
|
||||
public LegacyIndustrySelectionSnapshot Current => CreateSnapshot();
|
||||
@@ -104,7 +107,7 @@ public sealed class LegacyIndustrySelectionWorkflow
|
||||
Interlocked.Increment(ref _loadGeneration);
|
||||
_market = market;
|
||||
_industries = NoIndustries;
|
||||
_actions = LegacyIndustryActionCatalog.GetActions(market);
|
||||
_actions = _actionLayout.GetActions(market);
|
||||
_selectedIndex = null;
|
||||
_selectedIndustry = null;
|
||||
_firstIndustry = null;
|
||||
@@ -146,7 +149,7 @@ public sealed class LegacyIndustrySelectionWorkflow
|
||||
|
||||
_market = market;
|
||||
_industries = normalizedRows;
|
||||
_actions = LegacyIndustryActionCatalog.GetActions(market);
|
||||
_actions = _actionLayout.GetActions(market);
|
||||
_selectedIndustry = Reconcile(previousSelected, normalizedRows);
|
||||
_selectedIndex = _selectedIndustry is null
|
||||
? null
|
||||
|
||||
@@ -0,0 +1,745 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text;
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
|
||||
public enum LegacyRuntimeUiCatalogFailure
|
||||
{
|
||||
None,
|
||||
InvalidBaseDirectory,
|
||||
FileNotFound,
|
||||
FileUnavailable,
|
||||
FileTooLarge,
|
||||
InvalidEncoding,
|
||||
InvalidFormat,
|
||||
TooManySections,
|
||||
TooManyRows,
|
||||
UnknownEntry,
|
||||
DuplicateEntry,
|
||||
MissingEntry
|
||||
}
|
||||
|
||||
public sealed record LegacyRuntimeUiCatalogWarning(
|
||||
string FileName,
|
||||
LegacyRuntimeUiCatalogFailure Failure);
|
||||
|
||||
public sealed class LegacyIndustryActionLayout
|
||||
{
|
||||
private readonly IReadOnlyDictionary<IndustryMarket,
|
||||
IReadOnlyList<LegacyIndustryActionDefinition>> _actions;
|
||||
|
||||
private LegacyIndustryActionLayout(
|
||||
IReadOnlyDictionary<IndustryMarket,
|
||||
IReadOnlyList<LegacyIndustryActionDefinition>> actions)
|
||||
{
|
||||
_actions = actions;
|
||||
}
|
||||
|
||||
public static LegacyIndustryActionLayout Default { get; } = Create(
|
||||
LegacyIndustryActionCatalog.GetActions(IndustryMarket.Kospi),
|
||||
LegacyIndustryActionCatalog.GetActions(IndustryMarket.Kosdaq));
|
||||
|
||||
public IReadOnlyList<LegacyIndustryActionDefinition> GetActions(IndustryMarket market) =>
|
||||
_actions.TryGetValue(market, out var actions)
|
||||
? actions
|
||||
: throw new ArgumentOutOfRangeException(
|
||||
nameof(market), market, "Unsupported industry market.");
|
||||
|
||||
internal static LegacyIndustryActionLayout Create(
|
||||
IReadOnlyList<LegacyIndustryActionDefinition> kospi,
|
||||
IReadOnlyList<LegacyIndustryActionDefinition> kosdaq)
|
||||
{
|
||||
Validate(IndustryMarket.Kospi, kospi);
|
||||
Validate(IndustryMarket.Kosdaq, kosdaq);
|
||||
return new LegacyIndustryActionLayout(
|
||||
new ReadOnlyDictionary<IndustryMarket,
|
||||
IReadOnlyList<LegacyIndustryActionDefinition>>(
|
||||
new Dictionary<IndustryMarket,
|
||||
IReadOnlyList<LegacyIndustryActionDefinition>>
|
||||
{
|
||||
[IndustryMarket.Kospi] = Array.AsReadOnly(kospi.ToArray()),
|
||||
[IndustryMarket.Kosdaq] = Array.AsReadOnly(kosdaq.ToArray())
|
||||
}));
|
||||
}
|
||||
|
||||
private static void Validate(
|
||||
IndustryMarket market,
|
||||
IReadOnlyList<LegacyIndustryActionDefinition> actions)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(actions);
|
||||
var expected = LegacyIndustryActionCatalog.GetActions(market);
|
||||
if (actions.Count != expected.Count ||
|
||||
actions.Any(action => action is null || action.Market != market) ||
|
||||
!actions.Select(action => action.ActionId).ToHashSet(StringComparer.Ordinal)
|
||||
.SetEquals(expected.Select(action => action.ActionId)))
|
||||
{
|
||||
throw new InvalidDataException("The runtime industry action layout is invalid.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record LegacyComparisonActionPlacement(string ActionId, string Section);
|
||||
|
||||
public sealed class LegacyComparisonUiLayout
|
||||
{
|
||||
private readonly IReadOnlyList<string> _fixedTargetIds;
|
||||
private readonly IReadOnlyList<LegacyComparisonActionPlacement> _actionPlacements;
|
||||
|
||||
private LegacyComparisonUiLayout(
|
||||
IReadOnlyList<string> fixedTargetIds,
|
||||
IReadOnlyList<LegacyComparisonActionPlacement> actionPlacements)
|
||||
{
|
||||
_fixedTargetIds = fixedTargetIds;
|
||||
_actionPlacements = actionPlacements;
|
||||
}
|
||||
|
||||
public static LegacyComparisonUiLayout Default { get; } = Create(
|
||||
LegacyComparisonWorkflowCatalog.FixedTargets.Select(target => target.TargetId),
|
||||
LegacyComparisonWorkflowCatalog.Actions.Select(action =>
|
||||
new LegacyComparisonActionPlacement(action.ActionId, action.Section)));
|
||||
|
||||
internal IReadOnlyList<LegacyComparisonTarget> FixedTargets => _fixedTargetIds
|
||||
.Select(id => LegacyComparisonWorkflowCatalog.GetFixedTarget(id) ??
|
||||
throw new InvalidOperationException(
|
||||
"The runtime comparison layout contains an unknown target."))
|
||||
.ToArray();
|
||||
|
||||
internal IReadOnlyList<LegacyComparisonActionView> CreateActionViews(
|
||||
LegacyComparisonTarget? first,
|
||||
LegacyComparisonTarget? second) =>
|
||||
LegacyComparisonWorkflowCatalog.CreateActionViews(
|
||||
first,
|
||||
second,
|
||||
_actionPlacements);
|
||||
|
||||
internal static LegacyComparisonUiLayout Create(
|
||||
IEnumerable<string> fixedTargetIds,
|
||||
IEnumerable<LegacyComparisonActionPlacement> actionPlacements)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(fixedTargetIds);
|
||||
ArgumentNullException.ThrowIfNull(actionPlacements);
|
||||
var targetIds = fixedTargetIds.ToArray();
|
||||
var placements = actionPlacements.ToArray();
|
||||
var expectedTargetIds = LegacyComparisonWorkflowCatalog.FixedTargets
|
||||
.Select(target => target.TargetId)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
var expectedActionIds = LegacyComparisonWorkflowCatalog.Actions
|
||||
.Select(action => action.ActionId)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
if (targetIds.Length != expectedTargetIds.Count ||
|
||||
targetIds.Distinct(StringComparer.Ordinal).Count() != targetIds.Length ||
|
||||
!targetIds.ToHashSet(StringComparer.Ordinal).SetEquals(expectedTargetIds) ||
|
||||
placements.Length != expectedActionIds.Count ||
|
||||
placements.Any(placement =>
|
||||
placement is null ||
|
||||
string.IsNullOrWhiteSpace(placement.Section) ||
|
||||
placement.Section.Any(char.IsControl)) ||
|
||||
placements.Select(placement => placement.ActionId)
|
||||
.Distinct(StringComparer.Ordinal).Count() != placements.Length ||
|
||||
!placements.Select(placement => placement.ActionId)
|
||||
.ToHashSet(StringComparer.Ordinal).SetEquals(expectedActionIds))
|
||||
{
|
||||
throw new InvalidDataException("The runtime comparison UI layout is invalid.");
|
||||
}
|
||||
|
||||
return new LegacyComparisonUiLayout(
|
||||
Array.AsReadOnly(targetIds),
|
||||
Array.AsReadOnly(placements));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record LegacyRuntimeUiCatalogComposition(
|
||||
LegacyFixedActionCatalog FixedCatalog,
|
||||
LegacyIndustryActionLayout IndustryLayout,
|
||||
LegacyComparisonUiLayout ComparisonLayout,
|
||||
IReadOnlyList<LegacyRuntimeUiCatalogWarning> Warnings)
|
||||
{
|
||||
public string? WarningMessage => Warnings.Count == 0
|
||||
? null
|
||||
: "런타임 UI INI 일부를 읽지 못해 검증된 내장 목록을 사용합니다: " +
|
||||
string.Join(", ", Warnings.Select(warning =>
|
||||
$"{warning.FileName} ({warning.Failure})"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads only the six non-secret CP949 UI catalogs next to the executable. Raw INI text
|
||||
/// controls section and row order, while every executable action remains joined to one
|
||||
/// pre-registered C# identity. A malformed file can therefore change neither scene aliases
|
||||
/// nor mutation data and falls back independently to the embedded catalog.
|
||||
/// </summary>
|
||||
public static class LegacyRuntimeUiCatalogLoader
|
||||
{
|
||||
public const int MaximumFileBytes = 64 * 1024;
|
||||
public const int MaximumSections = 20;
|
||||
public const int MaximumRowsPerSection = 50;
|
||||
public const int MaximumLineLength = 256;
|
||||
|
||||
private static readonly Encoding Cp949 = CreateCp949Encoding();
|
||||
|
||||
public static LegacyRuntimeUiCatalogComposition ResolveFromBaseDirectory(
|
||||
string? baseDirectory)
|
||||
{
|
||||
var warnings = new List<LegacyRuntimeUiCatalogWarning>();
|
||||
if (!TryCreateResourceDirectory(baseDirectory, out var resourceDirectory))
|
||||
{
|
||||
warnings.Add(new LegacyRuntimeUiCatalogWarning(
|
||||
"Res",
|
||||
LegacyRuntimeUiCatalogFailure.InvalidBaseDirectory));
|
||||
return CreateFallback(warnings);
|
||||
}
|
||||
|
||||
var fixedCatalog = LegacyFixedActionCatalog.Default;
|
||||
var fixedOrders = new Dictionary<LegacyFixedMarket,
|
||||
IReadOnlyList<LegacyFixedAction>>();
|
||||
LoadFixed(
|
||||
resourceDirectory,
|
||||
"해외.ini",
|
||||
LegacyFixedMarket.Overseas,
|
||||
fixedCatalog,
|
||||
fixedOrders,
|
||||
warnings);
|
||||
LoadFixed(
|
||||
resourceDirectory,
|
||||
"환율.ini",
|
||||
LegacyFixedMarket.Exchange,
|
||||
fixedCatalog,
|
||||
fixedOrders,
|
||||
warnings);
|
||||
LoadFixed(
|
||||
resourceDirectory,
|
||||
"지수.ini",
|
||||
LegacyFixedMarket.Index,
|
||||
fixedCatalog,
|
||||
fixedOrders,
|
||||
warnings);
|
||||
fixedCatalog = fixedCatalog.WithRuntimeOrder(fixedOrders);
|
||||
|
||||
var kospi = LoadIndustry(
|
||||
resourceDirectory,
|
||||
"업종_코스피.ini",
|
||||
IndustryMarket.Kospi,
|
||||
warnings);
|
||||
var kosdaq = LoadIndustry(
|
||||
resourceDirectory,
|
||||
"업종_코스닥.ini",
|
||||
IndustryMarket.Kosdaq,
|
||||
warnings);
|
||||
var industryLayout = LegacyIndustryActionLayout.Create(kospi, kosdaq);
|
||||
|
||||
var comparisonLayout = LoadComparison(
|
||||
resourceDirectory,
|
||||
"종목비교.ini",
|
||||
warnings);
|
||||
return new LegacyRuntimeUiCatalogComposition(
|
||||
fixedCatalog,
|
||||
industryLayout,
|
||||
comparisonLayout,
|
||||
Array.AsReadOnly(warnings.ToArray()));
|
||||
}
|
||||
|
||||
private static LegacyRuntimeUiCatalogComposition CreateFallback(
|
||||
IReadOnlyList<LegacyRuntimeUiCatalogWarning> warnings) => new(
|
||||
LegacyFixedActionCatalog.Default,
|
||||
LegacyIndustryActionLayout.Default,
|
||||
LegacyComparisonUiLayout.Default,
|
||||
warnings);
|
||||
|
||||
private static void LoadFixed(
|
||||
string resourceDirectory,
|
||||
string fileName,
|
||||
LegacyFixedMarket market,
|
||||
LegacyFixedActionCatalog catalog,
|
||||
IDictionary<LegacyFixedMarket, IReadOnlyList<LegacyFixedAction>> orders,
|
||||
ICollection<LegacyRuntimeUiCatalogWarning> warnings)
|
||||
{
|
||||
if (TryReadDocument(
|
||||
Path.Combine(resourceDirectory, fileName),
|
||||
out var document,
|
||||
out var failure) &&
|
||||
TryJoinRows(
|
||||
document,
|
||||
catalog.GetActions(market),
|
||||
action => action.Section,
|
||||
action => action.Label,
|
||||
out var actions,
|
||||
out failure))
|
||||
{
|
||||
orders.Add(market, actions);
|
||||
return;
|
||||
}
|
||||
|
||||
orders.Add(market, catalog.GetActions(market));
|
||||
warnings.Add(new LegacyRuntimeUiCatalogWarning(fileName, failure));
|
||||
}
|
||||
|
||||
private static IReadOnlyList<LegacyIndustryActionDefinition> LoadIndustry(
|
||||
string resourceDirectory,
|
||||
string fileName,
|
||||
IndustryMarket market,
|
||||
ICollection<LegacyRuntimeUiCatalogWarning> warnings)
|
||||
{
|
||||
var embedded = LegacyIndustryActionCatalog.GetActions(market);
|
||||
if (!TryReadDocument(
|
||||
Path.Combine(resourceDirectory, fileName),
|
||||
out var document,
|
||||
out var failure))
|
||||
{
|
||||
warnings.Add(new LegacyRuntimeUiCatalogWarning(fileName, failure));
|
||||
return embedded;
|
||||
}
|
||||
|
||||
var expectedSection = market == IndustryMarket.Kospi ? "코스피" : "코스닥";
|
||||
if (document.Sections.Count != 1 ||
|
||||
!string.Equals(
|
||||
document.Sections[0].Name,
|
||||
expectedSection,
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
warnings.Add(new LegacyRuntimeUiCatalogWarning(
|
||||
fileName,
|
||||
LegacyRuntimeUiCatalogFailure.InvalidFormat));
|
||||
return embedded;
|
||||
}
|
||||
|
||||
var expected = embedded.Select(action => new IndustryJoinRow(
|
||||
expectedSection,
|
||||
action.Label,
|
||||
action)).ToArray();
|
||||
if (!TryJoinRows(
|
||||
document,
|
||||
expected,
|
||||
row => row.Section,
|
||||
row => row.Label,
|
||||
out var ordered,
|
||||
out failure))
|
||||
{
|
||||
warnings.Add(new LegacyRuntimeUiCatalogWarning(fileName, failure));
|
||||
return embedded;
|
||||
}
|
||||
|
||||
return Array.AsReadOnly(ordered
|
||||
.Select(row => row.Action with { Section = expectedSection })
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
private static LegacyComparisonUiLayout LoadComparison(
|
||||
string resourceDirectory,
|
||||
string fileName,
|
||||
ICollection<LegacyRuntimeUiCatalogWarning> warnings)
|
||||
{
|
||||
if (!TryReadDocument(
|
||||
Path.Combine(resourceDirectory, fileName),
|
||||
out var document,
|
||||
out var failure) ||
|
||||
!TryCreateComparisonLayout(document, out var layout, out failure))
|
||||
{
|
||||
warnings.Add(new LegacyRuntimeUiCatalogWarning(fileName, failure));
|
||||
return LegacyComparisonUiLayout.Default;
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
private static bool TryCreateComparisonLayout(
|
||||
IniDocument document,
|
||||
out LegacyComparisonUiLayout layout,
|
||||
out LegacyRuntimeUiCatalogFailure failure)
|
||||
{
|
||||
layout = LegacyComparisonUiLayout.Default;
|
||||
if (document.Sections.Count != 6 ||
|
||||
!string.Equals(document.Sections[0].Name, "지수", StringComparison.Ordinal))
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.InvalidFormat;
|
||||
return false;
|
||||
}
|
||||
|
||||
var targetByLabel = LegacyComparisonWorkflowCatalog.FixedTargets
|
||||
.ToDictionary(target => target.DisplayName, StringComparer.Ordinal);
|
||||
var targetIds = new List<string>(targetByLabel.Count);
|
||||
var targetLabels = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var label in document.Sections[0].Rows)
|
||||
{
|
||||
if (!targetLabels.Add(label))
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.DuplicateEntry;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!targetByLabel.TryGetValue(label, out var target))
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.UnknownEntry;
|
||||
return false;
|
||||
}
|
||||
|
||||
targetIds.Add(target.TargetId);
|
||||
}
|
||||
|
||||
if (targetIds.Count != targetByLabel.Count)
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.MissingEntry;
|
||||
return false;
|
||||
}
|
||||
|
||||
var actions = LegacyComparisonWorkflowCatalog.Actions;
|
||||
var directActions = actions
|
||||
.Where(action => action.Kind != LegacyComparisonActionKind.Return)
|
||||
.ToDictionary(action => action.Label, StringComparer.Ordinal);
|
||||
var returnActions = actions
|
||||
.Where(action => action.Kind == LegacyComparisonActionKind.Return)
|
||||
.ToArray();
|
||||
var returnSectionNames = returnActions
|
||||
.Select(action => action.Section)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (returnSectionNames.Length != 1)
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.InvalidFormat;
|
||||
return false;
|
||||
}
|
||||
|
||||
var expectedSectionNames = directActions.Keys
|
||||
.Append(returnSectionNames[0])
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
var actualSectionNames = document.Sections.Skip(1)
|
||||
.Select(section => section.Name)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
if (actualSectionNames.Count != expectedSectionNames.Count ||
|
||||
!actualSectionNames.SetEquals(expectedSectionNames))
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.UnknownEntry;
|
||||
return false;
|
||||
}
|
||||
|
||||
var placements = new List<LegacyComparisonActionPlacement>(actions.Count);
|
||||
var actionIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var section in document.Sections.Skip(1))
|
||||
{
|
||||
if (directActions.TryGetValue(section.Name, out var directAction))
|
||||
{
|
||||
if (section.Rows.Count != 0)
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.InvalidFormat;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!actionIds.Add(directAction.ActionId))
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.DuplicateEntry;
|
||||
return false;
|
||||
}
|
||||
|
||||
placements.Add(new LegacyComparisonActionPlacement(
|
||||
directAction.ActionId,
|
||||
section.Name));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.Equals(
|
||||
section.Name,
|
||||
returnSectionNames[0],
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.UnknownEntry;
|
||||
return false;
|
||||
}
|
||||
|
||||
var expectedReturnRows = returnActions.Select(action =>
|
||||
new ComparisonJoinRow(section.Name, action.Label, action)).ToArray();
|
||||
var returnDocument = new IniDocument(
|
||||
Array.AsReadOnly(new[] { section }));
|
||||
if (!TryJoinRows(
|
||||
returnDocument,
|
||||
expectedReturnRows,
|
||||
row => row.Section,
|
||||
row => row.Label,
|
||||
out var orderedReturns,
|
||||
out failure))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var row in orderedReturns)
|
||||
{
|
||||
actionIds.Add(row.Action.ActionId);
|
||||
placements.Add(new LegacyComparisonActionPlacement(
|
||||
row.Action.ActionId,
|
||||
section.Name));
|
||||
}
|
||||
}
|
||||
|
||||
if (placements.Count != actions.Count || actionIds.Count != actions.Count)
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.MissingEntry;
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
layout = LegacyComparisonUiLayout.Create(targetIds, placements);
|
||||
failure = LegacyRuntimeUiCatalogFailure.None;
|
||||
return true;
|
||||
}
|
||||
catch (InvalidDataException)
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.InvalidFormat;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryJoinRows<T>(
|
||||
IniDocument document,
|
||||
IReadOnlyList<T> expected,
|
||||
Func<T, string> sectionSelector,
|
||||
Func<T, string> labelSelector,
|
||||
out IReadOnlyList<T> ordered,
|
||||
out LegacyRuntimeUiCatalogFailure failure)
|
||||
{
|
||||
ordered = Array.Empty<T>();
|
||||
var expectedSections = expected.Select(sectionSelector)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
var actualSections = document.Sections.Select(section => section.Name)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
if (document.Sections.Count != expectedSections.Count ||
|
||||
!actualSections.SetEquals(expectedSections))
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.InvalidFormat;
|
||||
return false;
|
||||
}
|
||||
|
||||
var expectedOccurrences = new Dictionary<EntryKey, T>();
|
||||
var expectedCounts = new Dictionary<EntryPair, int>();
|
||||
foreach (var value in expected)
|
||||
{
|
||||
var pair = new EntryPair(sectionSelector(value), labelSelector(value));
|
||||
expectedCounts.TryGetValue(pair, out var occurrence);
|
||||
expectedCounts[pair] = occurrence + 1;
|
||||
expectedOccurrences.Add(
|
||||
new EntryKey(pair.Section, pair.Label, occurrence),
|
||||
value);
|
||||
}
|
||||
|
||||
var result = new List<T>(expected.Count);
|
||||
var actualCounts = new Dictionary<EntryPair, int>();
|
||||
foreach (var section in document.Sections)
|
||||
{
|
||||
foreach (var label in section.Rows)
|
||||
{
|
||||
var pair = new EntryPair(section.Name, label);
|
||||
actualCounts.TryGetValue(pair, out var occurrence);
|
||||
actualCounts[pair] = occurrence + 1;
|
||||
if (!expectedOccurrences.TryGetValue(
|
||||
new EntryKey(section.Name, label, occurrence),
|
||||
out var value))
|
||||
{
|
||||
failure = expectedCounts.ContainsKey(pair)
|
||||
? LegacyRuntimeUiCatalogFailure.DuplicateEntry
|
||||
: LegacyRuntimeUiCatalogFailure.UnknownEntry;
|
||||
return false;
|
||||
}
|
||||
|
||||
result.Add(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.Count != expected.Count)
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.MissingEntry;
|
||||
return false;
|
||||
}
|
||||
|
||||
ordered = Array.AsReadOnly(result.ToArray());
|
||||
failure = LegacyRuntimeUiCatalogFailure.None;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadDocument(
|
||||
string path,
|
||||
out IniDocument document,
|
||||
out LegacyRuntimeUiCatalogFailure failure)
|
||||
{
|
||||
document = IniDocument.Empty;
|
||||
byte[] bytes;
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(
|
||||
path,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.Read,
|
||||
4096,
|
||||
FileOptions.SequentialScan);
|
||||
if (stream.Length > MaximumFileBytes)
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.FileTooLarge;
|
||||
return false;
|
||||
}
|
||||
|
||||
bytes = new byte[(int)stream.Length];
|
||||
stream.ReadExactly(bytes);
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is FileNotFoundException or DirectoryNotFoundException)
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.FileNotFound;
|
||||
return false;
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is IOException or UnauthorizedAccessException or
|
||||
ArgumentException or NotSupportedException or
|
||||
System.Security.SecurityException)
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.FileUnavailable;
|
||||
return false;
|
||||
}
|
||||
|
||||
string text;
|
||||
try
|
||||
{
|
||||
text = Cp949.GetString(bytes);
|
||||
}
|
||||
catch (DecoderFallbackException)
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.InvalidEncoding;
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryParse(text, out document, out failure);
|
||||
}
|
||||
|
||||
private static bool TryParse(
|
||||
string text,
|
||||
out IniDocument document,
|
||||
out LegacyRuntimeUiCatalogFailure failure)
|
||||
{
|
||||
document = IniDocument.Empty;
|
||||
var sections = new List<IniSection>();
|
||||
var sectionNames = new HashSet<string>(StringComparer.Ordinal);
|
||||
List<string>? currentRows = null;
|
||||
using var reader = new StringReader(text);
|
||||
while (reader.ReadLine() is { } sourceLine)
|
||||
{
|
||||
var line = sourceLine.Trim();
|
||||
if (line.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.Length > MaximumLineLength ||
|
||||
line.Any(character => char.IsControl(character) || char.IsSurrogate(character)))
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.InvalidFormat;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (line[0] == '[')
|
||||
{
|
||||
if (line.Length < 3 || line[^1] != ']' ||
|
||||
line.AsSpan(1, line.Length - 2).Contains('[') ||
|
||||
line.AsSpan(1, line.Length - 2).Contains(']'))
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.InvalidFormat;
|
||||
return false;
|
||||
}
|
||||
|
||||
var name = line[1..^1].Trim();
|
||||
if (name.Length == 0 || !sectionNames.Add(name))
|
||||
{
|
||||
failure = name.Length == 0
|
||||
? LegacyRuntimeUiCatalogFailure.InvalidFormat
|
||||
: LegacyRuntimeUiCatalogFailure.DuplicateEntry;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sections.Count >= MaximumSections)
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.TooManySections;
|
||||
return false;
|
||||
}
|
||||
|
||||
currentRows = [];
|
||||
sections.Add(new IniSection(name, currentRows));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentRows is null)
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.InvalidFormat;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentRows.Count >= MaximumRowsPerSection)
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.TooManyRows;
|
||||
return false;
|
||||
}
|
||||
|
||||
currentRows.Add(line);
|
||||
}
|
||||
|
||||
if (sections.Count == 0)
|
||||
{
|
||||
failure = LegacyRuntimeUiCatalogFailure.InvalidFormat;
|
||||
return false;
|
||||
}
|
||||
|
||||
document = new IniDocument(Array.AsReadOnly(sections.ToArray()));
|
||||
failure = LegacyRuntimeUiCatalogFailure.None;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryCreateResourceDirectory(
|
||||
string? baseDirectory,
|
||||
out string resourceDirectory)
|
||||
{
|
||||
resourceDirectory = string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(baseDirectory) ||
|
||||
baseDirectory.Any(char.IsControl) ||
|
||||
!Path.IsPathFullyQualified(baseDirectory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
resourceDirectory = Path.Combine(Path.GetFullPath(baseDirectory), "Res");
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is ArgumentException or IOException or NotSupportedException or
|
||||
System.Security.SecurityException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Encoding CreateCp949Encoding()
|
||||
{
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
return Encoding.GetEncoding(
|
||||
949,
|
||||
EncoderFallback.ExceptionFallback,
|
||||
DecoderFallback.ExceptionFallback);
|
||||
}
|
||||
|
||||
private sealed record IniSection(string Name, IReadOnlyList<string> Rows);
|
||||
|
||||
private sealed record IniDocument(IReadOnlyList<IniSection> Sections)
|
||||
{
|
||||
public static IniDocument Empty { get; } = new(Array.Empty<IniSection>());
|
||||
}
|
||||
|
||||
private sealed record IndustryJoinRow(
|
||||
string Section,
|
||||
string Label,
|
||||
LegacyIndustryActionDefinition Action);
|
||||
|
||||
private sealed record ComparisonJoinRow(
|
||||
string Section,
|
||||
string Label,
|
||||
LegacyComparisonActionDefinition Action);
|
||||
|
||||
private readonly record struct EntryPair(string Section, string Label);
|
||||
|
||||
private readonly record struct EntryKey(string Section, string Label, int Occurrence);
|
||||
}
|
||||
Reference in New Issue
Block a user