/* ======================================================================== * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * ======================================================================*/ using System; using System.Globalization; using System.IO; using Opc.Ua; using Opc.Ua.DataChannels; namespace ConsoleDataChannelStreaming { /// /// Explicit finite application choices, independent of carrier negotiation. /// internal sealed record SampleOptions { /// /// Gets the selected carrier; unsupported selections never fall back. /// public DataChannelCarrierKind Carrier { get; init; } = DataChannelCarrierKind.DedicatedUasc; /// /// Gets whether normal Session Calls additionally authorize the flow. /// public bool SessionAuthorization { get; init; } /// /// Gets whether the connecting application, rather than the listening application, sends payload. /// public bool ClientPublishes { get; init; } /// /// Gets the number of complete opaque messages. /// public int Count { get; init; } = 100; /// /// Gets the complete application message length. /// public int Size { get; init; } = 1200; /// /// Gets optional pacing, not a transport delivery guarantee. /// public int IntervalMilliseconds { get; init; } /// /// Gets the finite deadline, including certificate setup and shutdown. /// public int TimeoutSeconds { get; init; } = 60; /// /// Gets an optional cancellation demonstration deadline starting only after the flow is ready. /// public int CancelAfterMilliseconds { get; init; } /// /// Gets whether the opener commits a new priority halfway through the run. /// public bool Revise { get; init; } /// /// Gets whether to compare shared-channel runs with and without competing Services. /// public bool Benchmark { get; init; } /// /// Gets the bounded number of benchmark repetitions. /// public int Repeat { get; init; } = 1; /// /// Gets how many real monitored items generate Publish notifications. /// public int MonitoredItems { get; init; } = 4; /// /// Gets the requested publishing interval; the actual revised value is reported. /// public int PublishingInterval { get; init; } = 50; /// /// Gets whether ordinary Read and Publish traffic is enabled on a shared connection. /// public bool ServiceLoad { get; init; } = true; /// /// Gets a fresh, exclusively owned state directory, removed after the run. /// public string? StateDirectory { get; init; } /// /// Gets the explicit reviewed native QUIC asset. /// public string? QuicLibrary { get; init; } /// /// Gets the independently supplied expected QUIC asset SHA-256. /// public string? QuicSha256 { get; init; } /// /// Gets the explicit reviewed native HTTP/3 asset. /// public string? Http3Library { get; init; } /// /// Gets the independently supplied expected HTTP/3 asset SHA-256. /// public string? Http3Sha256 { get; init; } /// /// Gets whether only command-line help was requested. /// public bool ShowHelp { get; init; } /// /// Gets whether this carrier has an ordinary Service lane. /// public bool Shared => Carrier == DataChannelCarrierKind.SharedUasc; /// /// Parses strict choices rather than interpreting unknown input as a default transport. /// public static SampleOptions Parse(string[] arguments) { var options = new SampleOptions { QuicLibrary = Environment.GetEnvironmentVariable("OPCUA_QUIC_LIBRARY") ?? Environment.GetEnvironmentVariable("OPCUA_QUIC_NATIVE_PATH"), QuicSha256 = Environment.GetEnvironmentVariable("OPCUA_QUIC_SHA256") ?? Environment.GetEnvironmentVariable("OPCUA_QUIC_NATIVE_SHA256"), Http3Library = Environment.GetEnvironmentVariable("OPCUA_HTTP3_LIBRARY") ?? Environment.GetEnvironmentVariable("OPCUA_HTTP3_NATIVE_PATH"), Http3Sha256 = Environment.GetEnvironmentVariable("OPCUA_HTTP3_SHA256") ?? Environment.GetEnvironmentVariable("OPCUA_HTTP3_NATIVE_SHA256") }; for (int index = 0; index < arguments.Length; index++) { string argument = arguments[index]; options = argument switch { "--help" or "-h" => options with { ShowHelp = true }, "--carrier" => options with { Carrier = ParseCarrier(Next(arguments, ref index)) }, "--authorization" => options with { SessionAuthorization = Choice(Next(arguments, ref index), "configured", "session") }, "--publisher" => options with { ClientPublishes = Choice(Next(arguments, ref index), "server", "client") }, "--mode" => options with { Benchmark = Choice(Next(arguments, ref index), "stream", "benchmark") }, "--count" => options with { Count = Number(Next(arguments, ref index), 1, 1_000_000) }, "--size" => options with { Size = Number(Next(arguments, ref index), 4, 32768) }, "--interval-ms" => options with { IntervalMilliseconds = Number(Next(arguments, ref index), 0, 1000) }, "--timeout" => options with { TimeoutSeconds = Number(Next(arguments, ref index), 1, 300) }, "--cancel-after-ms" => options with { CancelAfterMilliseconds = Number(Next(arguments, ref index), 1, 60000) }, "--repeat" => options with { Repeat = Number(Next(arguments, ref index), 1, 10) }, "--monitored-items" => options with { MonitoredItems = Number(Next(arguments, ref index), 1, 1000) }, "--publishing-interval" => options with { PublishingInterval = Number(Next(arguments, ref index), 10, 1000) }, "--state" => options with { StateDirectory = Next(arguments, ref index) }, "--quic-library" => options with { QuicLibrary = Next(arguments, ref index) }, "--quic-sha256" => options with { QuicSha256 = Next(arguments, ref index) }, "--http3-library" => options with { Http3Library = Next(arguments, ref index) }, "--http3-sha256" => options with { Http3Sha256 = Next(arguments, ref index) }, "--revise" => options with { Revise = true }, _ => throw new ArgumentException($"Unknown argument '{argument}'. Use --help.") }; } if (!options.ShowHelp) { options.Validate(); } return options; } /// /// Validates application combinations before creating certificates or opening sockets. /// public void Validate() { if (Revise && Count < 2) { throw new ArgumentException("--revise requires at least two messages."); } if (Benchmark && !Shared) { throw new ArgumentException("The competing Read/Publish benchmark requires --carrier shared."); } if (Carrier is DataChannelCarrierKind.Quic or DataChannelCarrierKind.WebTransport) { ValidateAsset(QuicLibrary, QuicSha256, "QUIC"); } if (Carrier == DataChannelCarrierKind.WebTransport) { ValidateAsset(Http3Library, Http3Sha256, "HTTP/3"); } } /// /// Prints only supported finite modes. /// public static void PrintUsage() { Console.WriteLine( "ConsoleDataChannelStreaming (.NET 10)\n" + " --carrier dedicated|shared|quic|webtransport (default dedicated)\n" + " --authorization configured|session (default configured)\n" + " --publisher server|client (default server; client always opens the flow)\n" + " --count N --size N --interval-ms N --timeout SECONDS\n" + " --cancel-after-ms N (cancel after traffic starts; expected nonzero exit)\n" + " --revise (commit a live priority revision halfway through the messages)\n" + " --mode stream|benchmark --repeat N --monitored-items N --publishing-interval MS\n" + " --state NEW-DIRECTORY (owned PKI; removed at shutdown)\n" + " --quic-library ABSOLUTE-PATH --quic-sha256 EXPECTED-HASH\n" + " --http3-library ABSOLUTE-PATH --http3-sha256 EXPECTED-HASH\n" + "Native assets may also be selected with OPCUA_QUIC_LIBRARY/OPCUA_QUIC_SHA256 and\n" + "OPCUA_HTTP3_LIBRARY/OPCUA_HTTP3_SHA256. No fallback, implicit UASC, or browser mode."); } /// /// Rejects missing or unreviewed assets without substituting another carrier. /// private static void ValidateAsset(string? path, string? hash, string name) { if (string.IsNullOrWhiteSpace(path) || !Path.IsPathFullyQualified(path) || !File.Exists(path) || !IsSha256(hash)) { throw new ArgumentException( $"{name} requires an explicit existing absolute library path and reviewed SHA-256; no fallback."); } } /// /// Validates the expected digest without accepting a locally computed replacement. /// private static bool IsSha256(string? value) { if (value == null || value.Length != 64) { return false; } foreach (char character in value) { if (!char.IsAsciiHexDigit(character)) { return false; } } return true; } /// /// Reads a required argument value. /// private static string Next(string[] arguments, ref int index) { if (++index >= arguments.Length || string.IsNullOrWhiteSpace(arguments[index])) { throw new ArgumentException("An option value is missing."); } return arguments[index]; } /// /// Parses a finite integer range without coercion. /// private static int Number(string value, int minimum, int maximum) { if (!int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out int number) || number < minimum || number > maximum) { throw new ArgumentException($"'{value}' must be an integer in [{minimum}, {maximum}]."); } return number; } /// /// Parses one of two explicit choices. /// private static bool Choice(string value, string first, string second) { return value == first ? false : value == second ? true : throw new ArgumentException($"'{value}' must be '{first}' or '{second}'."); } /// /// Keeps carrier selection independent of payload and authorization choices. /// private static DataChannelCarrierKind ParseCarrier(string value) { return value switch { "dedicated" => DataChannelCarrierKind.DedicatedUasc, "shared" => DataChannelCarrierKind.SharedUasc, "quic" => DataChannelCarrierKind.Quic, "webtransport" => DataChannelCarrierKind.WebTransport, _ => throw new ArgumentException($"Unknown carrier '{value}'. No transport fallback is available.") }; } } }