feat: load legacy Cuts and Res runtime assets

This commit is contained in:
2026-07-18 14:32:04 +09:00
parent da8da06fac
commit 5eb4054120
32 changed files with 2893 additions and 120 deletions

View File

@@ -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();

View File

@@ -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,

View File

@@ -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)

View File

@@ -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

View File

@@ -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);
}