fix(driver-abcip-cli): resolve Medium code-review findings (Driver.AbCip.Cli-001, -002)

Driver.AbCip.Cli-001: WriteCommand.ParseValue wraps FormatException/
OverflowException as CommandException so bad --value input yields a clean
CLI error instead of a raw stack trace.
Driver.AbCip.Cli-002: probe/read/subscribe commands reject Structure types
up front (RejectStructure helper), matching the write guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-22 09:14:41 -04:00
parent e8edf123ff
commit 29e656912e
7 changed files with 78 additions and 22 deletions

View File

@@ -56,4 +56,19 @@ public abstract class AbCipCommandBase : DriverCommandBase
/// multiple gateways in parallel can distinguish the logs.
/// </summary>
protected string DriverInstanceId => $"abcip-cli-{Gateway}";
/// <summary>
/// Guards against <see cref="AbCipDataType.Structure"/> being passed to a command
/// that does not support UDT layouts. Call at the top of <c>ExecuteAsync</c> for any
/// command that accepts <c>--type</c> but cannot handle memberless Structure tags.
/// Throws a <see cref="CliFx.Exceptions.CommandException"/> if <paramref name="type"/>
/// is <see cref="AbCipDataType.Structure"/>.
/// </summary>
protected static void RejectStructure(AbCipDataType type)
{
if (type == AbCipDataType.Structure)
throw new CliFx.Exceptions.CommandException(
"Structure (UDT) reads are out of scope for this command — those need an explicit " +
"member layout, which belongs in a real driver config.");
}
}

View File

@@ -25,6 +25,7 @@ public sealed class ProbeCommand : AbCipCommandBase
public override async ValueTask ExecuteAsync(IConsole console)
{
ConfigureLogging();
RejectStructure(DataType);
var ct = console.RegisterCancellationHandler();
var probeTag = new AbCipTagDefinition(

View File

@@ -27,6 +27,7 @@ public sealed class ReadCommand : AbCipCommandBase
public override async ValueTask ExecuteAsync(IConsole console)
{
ConfigureLogging();
RejectStructure(DataType);
var ct = console.RegisterCancellationHandler();
var tagName = SynthesiseTagName(TagPath, DataType);

View File

@@ -30,6 +30,7 @@ public sealed class SubscribeCommand : AbCipCommandBase
public override async ValueTask ExecuteAsync(IConsole console)
{
ConfigureLogging();
RejectStructure(DataType);
var ct = console.RegisterCancellationHandler();
var tagName = ReadCommand.SynthesiseTagName(TagPath, DataType);

View File

@@ -66,23 +66,40 @@ public sealed class WriteCommand : AbCipCommandBase
/// <summary>
/// Parse the operator's <c>--value</c> string into the CLR type the driver expects
/// for the declared <see cref="AbCipDataType"/>. Invariant culture everywhere.
/// Bad input (non-numeric text, out-of-range value) is caught and rethrown as a
/// <see cref="CliFx.Exceptions.CommandException"/> so CliFx renders a clean one-line
/// error rather than a full .NET stack trace.
/// </summary>
internal static object ParseValue(string raw, AbCipDataType type) => type switch
internal static object ParseValue(string raw, AbCipDataType type)
{
AbCipDataType.Bool => ParseBool(raw),
AbCipDataType.SInt => sbyte.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.Int => short.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.DInt or AbCipDataType.Dt => int.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.LInt => long.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.USInt => byte.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.UInt => ushort.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.UDInt => uint.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.ULInt => ulong.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.Real => float.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.LReal => double.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.String => raw,
_ => throw new CliFx.Exceptions.CommandException($"Unsupported DataType '{type}' for write."),
};
try
{
return type switch
{
AbCipDataType.Bool => ParseBool(raw),
AbCipDataType.SInt => sbyte.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.Int => short.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.DInt or AbCipDataType.Dt => int.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.LInt => long.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.USInt => byte.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.UInt => ushort.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.UDInt => uint.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.ULInt => ulong.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.Real => float.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.LReal => double.Parse(raw, CultureInfo.InvariantCulture),
AbCipDataType.String => raw,
_ => throw new CliFx.Exceptions.CommandException($"Unsupported DataType '{type}' for write."),
};
}
catch (Exception ex) when (ex is FormatException or OverflowException)
{
throw new CliFx.Exceptions.CommandException(
$"Cannot parse '{raw}' as {type}. " +
$"Check the value is within the valid range for {type} and uses invariant-culture " +
$"decimal notation (e.g. '3.14', not '3,14').",
innerException: ex);
}
}
private static bool ParseBool(string raw) => raw.Trim().ToLowerInvariant() switch
{