namespace MBN_STOCK_WEBVIEW.PlayoutSmoke; internal enum SmokeCommandKind { Invalid, Probe, DryRun, TestPlan, TestConnect, TestSequence, PgmConnectDiagnostic, PgmCutsSequence } internal sealed record SmokeCommandInvocation( SmokeCommandKind Kind, string? ConfigPath = null, string? PrepareSceneCode = null, string? NextSceneCode = null, int? ObservationMilliseconds = null, string? Host = null, int? Port = null, string? ExpectedPgmWindowTitle = null, string? SceneRoot = null); internal sealed record SmokeCommandParseResult( bool IsValid, SmokeCommandInvocation Invocation, string? ErrorCode = null); internal static class SmokeCommandLine { internal const string AcknowledgementFlag = "--acknowledge-isolated-non-program-test-output"; internal const string PgmConnectAcknowledgementFlag = "--i-understand-this-will-contact-current-pgm-tornado-via-ktap-connect-and-disconnect-only"; internal const string PgmCutsSequenceAcknowledgementFlag = "--i-understand-this-will-render-current-pgm-tornado-cuts-5001-then-5006-and-take-them-out"; public static SmokeCommandParseResult Parse(IReadOnlyList arguments) { ArgumentNullException.ThrowIfNull(arguments); if (arguments.Count == 0) { return Valid(new SmokeCommandInvocation(SmokeCommandKind.Probe)); } if (arguments.Count == 1) { return arguments[0].ToLowerInvariant() switch { "--probe" => Valid(new SmokeCommandInvocation(SmokeCommandKind.Probe)), "--dry-run" => Valid(new SmokeCommandInvocation(SmokeCommandKind.DryRun)), _ => Invalid("unknown-command") }; } var kind = arguments[0].ToLowerInvariant() switch { "--test-plan" => SmokeCommandKind.TestPlan, "--test-connect" => SmokeCommandKind.TestConnect, "--test-sequence" => SmokeCommandKind.TestSequence, "--pgm-connect-diagnostic" => SmokeCommandKind.PgmConnectDiagnostic, "--pgm-cuts-sequence" => SmokeCommandKind.PgmCutsSequence, _ => SmokeCommandKind.Invalid }; if (kind == SmokeCommandKind.Invalid) { return Invalid("unknown-command"); } string? configPath = null; string? prepareSceneCode = null; string? nextSceneCode = null; int? observationMilliseconds = null; string? host = null; int? port = null; string? expectedPgmWindowTitle = null; string? sceneRoot = null; var acknowledged = false; for (var index = 1; index < arguments.Count; index++) { var argument = arguments[index]; switch (argument) { case AcknowledgementFlag when kind is not ( SmokeCommandKind.PgmConnectDiagnostic or SmokeCommandKind.PgmCutsSequence): case PgmConnectAcknowledgementFlag when kind == SmokeCommandKind.PgmConnectDiagnostic: case PgmCutsSequenceAcknowledgementFlag when kind == SmokeCommandKind.PgmCutsSequence: if (acknowledged) { return Invalid("duplicate-option"); } acknowledged = true; break; case "--host" when kind is SmokeCommandKind.PgmConnectDiagnostic or SmokeCommandKind.PgmCutsSequence: if (host is not null) { return Invalid("duplicate-option"); } if (!TryReadValue(arguments, ref index, out host)) { return Invalid("missing-option-value"); } break; case "--port" when kind is SmokeCommandKind.PgmConnectDiagnostic or SmokeCommandKind.PgmCutsSequence: if (port is not null) { return Invalid("duplicate-option"); } if (!TryReadValue(arguments, ref index, out var portText) || !int.TryParse( portText, System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture, out var parsedPort) || parsedPort is < 1 or > 65_535) { return Invalid("invalid-port"); } port = parsedPort; break; case "--expected-pgm-window-title" when kind is SmokeCommandKind.PgmConnectDiagnostic or SmokeCommandKind.PgmCutsSequence: if (expectedPgmWindowTitle is not null) { return Invalid("duplicate-option"); } if (!TryReadValue(arguments, ref index, out expectedPgmWindowTitle)) { return Invalid("missing-option-value"); } break; case "--scene-root" when kind == SmokeCommandKind.PgmCutsSequence: if (sceneRoot is not null) { return Invalid("duplicate-option"); } if (!TryReadValue(arguments, ref index, out sceneRoot)) { return Invalid("missing-option-value"); } break; case "--config" when kind is not ( SmokeCommandKind.PgmConnectDiagnostic or SmokeCommandKind.PgmCutsSequence): if (configPath is not null) { return Invalid("duplicate-option"); } if (!TryReadValue(arguments, ref index, out configPath)) { return Invalid("missing-option-value"); } break; case "--prepare" when kind is SmokeCommandKind.TestPlan or SmokeCommandKind.TestSequence: if (prepareSceneCode is not null) { return Invalid("duplicate-option"); } if (!TryReadValue(arguments, ref index, out prepareSceneCode)) { return Invalid("missing-option-value"); } break; case "--next" when kind is SmokeCommandKind.TestPlan or SmokeCommandKind.TestSequence: if (nextSceneCode is not null) { return Invalid("duplicate-option"); } if (!TryReadValue(arguments, ref index, out nextSceneCode)) { return Invalid("missing-option-value"); } break; case "--observe-ms" when kind is SmokeCommandKind.TestPlan or SmokeCommandKind.TestSequence: if (observationMilliseconds is not null) { return Invalid("duplicate-option"); } if (!TryReadValue(arguments, ref index, out var observationText) || !int.TryParse( observationText, System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture, out var observationValue) || observationValue is < 1_000 or > 30_000) { return Invalid("invalid-observation-duration"); } observationMilliseconds = observationValue; break; case "--observe-ms" when kind == SmokeCommandKind.PgmCutsSequence: if (observationMilliseconds is not null) { return Invalid("duplicate-option"); } if (!TryReadValue(arguments, ref index, out var pgmObservationText) || !int.TryParse( pgmObservationText, System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture, out var pgmObservationValue) || pgmObservationValue != 5_000) { return Invalid("invalid-observation-duration"); } observationMilliseconds = pgmObservationValue; break; default: return Invalid("unknown-option"); } } if (!acknowledged) { return Invalid("missing-acknowledgement"); } if (kind == SmokeCommandKind.PgmConnectDiagnostic) { if (string.IsNullOrWhiteSpace(host) || port is null || string.IsNullOrWhiteSpace(expectedPgmWindowTitle)) { return Invalid("missing-required-option"); } return Valid(new SmokeCommandInvocation( kind, Host: host, Port: port, ExpectedPgmWindowTitle: expectedPgmWindowTitle)); } if (kind == SmokeCommandKind.PgmCutsSequence) { if (string.IsNullOrWhiteSpace(host) || !System.Net.IPAddress.TryParse(host, out var literalHost) || !System.Net.IPAddress.IsLoopback(literalHost) || port is null || string.IsNullOrWhiteSpace(expectedPgmWindowTitle) || string.IsNullOrWhiteSpace(sceneRoot) || !Path.IsPathFullyQualified(sceneRoot)) { return Invalid("missing-or-invalid-required-option"); } return Valid(new SmokeCommandInvocation( kind, ObservationMilliseconds: observationMilliseconds ?? 5_000, Host: host, Port: port, ExpectedPgmWindowTitle: expectedPgmWindowTitle, SceneRoot: sceneRoot)); } if (string.IsNullOrWhiteSpace(configPath)) { return Invalid("missing-required-option"); } if (kind is SmokeCommandKind.TestPlan or SmokeCommandKind.TestSequence) { if (prepareSceneCode is null || nextSceneCode is null || observationMilliseconds is null) { return Invalid("missing-required-option"); } if (!IsSafeSceneCode(prepareSceneCode) || !IsSafeSceneCode(nextSceneCode)) { return Invalid("unsafe-scene-code"); } if (string.Equals( prepareSceneCode, nextSceneCode, StringComparison.OrdinalIgnoreCase)) { return Invalid("duplicate-scene-code"); } } return Valid(new SmokeCommandInvocation( kind, configPath, prepareSceneCode, nextSceneCode, observationMilliseconds, host, port, expectedPgmWindowTitle, sceneRoot)); } internal static bool IsSafeSceneCode(string value) { if (value.Length is < 1 or > 64) { return false; } foreach (var character in value) { if (!((character >= 'A' && character <= 'Z') || (character >= 'a' && character <= 'z') || (character >= '0' && character <= '9') || character is '_' or '-')) { return false; } } return true; } internal static bool IsUnsafeTornadoWindowTitle(string? title) { if (string.IsNullOrWhiteSpace(title) || title.Contains("PGM", StringComparison.OrdinalIgnoreCase) || title.Contains("PROGRAM", StringComparison.OrdinalIgnoreCase)) { return true; } const string marker = "TEST"; for (var start = 0; start <= title.Length - marker.Length; start++) { if (!title.AsSpan(start, marker.Length).Equals( marker.AsSpan(), StringComparison.OrdinalIgnoreCase)) { continue; } var beforeIsBoundary = start == 0 || !char.IsLetterOrDigit(title[start - 1]); var after = start + marker.Length; var afterIsBoundary = after == title.Length || !char.IsLetterOrDigit(title[after]); if (beforeIsBoundary && afterIsBoundary) { return false; } } return true; } private static bool TryReadValue( IReadOnlyList arguments, ref int index, out string? value) { if (index + 1 >= arguments.Count || string.IsNullOrWhiteSpace(arguments[index + 1]) || arguments[index + 1].StartsWith("--", StringComparison.Ordinal)) { value = null; return false; } value = arguments[++index]; return true; } private static SmokeCommandParseResult Valid(SmokeCommandInvocation invocation) => new(true, invocation); private static SmokeCommandParseResult Invalid(string errorCode) => new(false, new SmokeCommandInvocation(SmokeCommandKind.Invalid), errorCode); }