fix(tagconfig): a numerically-spelled enum names no member — reject it fleet-wide (Driver.Sql.Contracts-002)
TagConfigJson.ClassifyEnumText classified via Enum.TryParse(text, ignoreCase: true), which accepts a numeric STRING for any enum: "1" returns the member at that ordinal and an out-of-range "99" parses just as happily. Every driver contract's strict enum read inherits that, so a Sql tag authored "type":"99" was ACCEPTED, carried (DriverDataType)99, and SqlPollReader.TryCoerce fell through its switch to the String arm — a stringified cell published Good under a type nothing can map. Refuse a digit- or sign-leading spelling before the parse (judged on the TRIMMED text, because Enum.TryParse trims), and require Enum.IsDefined — which covers what the leading-character check cannot, since TryParse also ORs a comma-separated list for a non-[Flags] enum. Mirrors RawBrowseCommitMapper.ParseNativeType and MTConnectTagDefinitionFactory.TryParseDataType, which already guard their own parses. Three AdminUI restatements of the same rule now ask ClassifyEnumText instead of Enum.TryParse (TagConfigJson.GetEnum, SqlTagConfigModel.ReadEnumStrict, MqttTagConfigModel.DescribeInvalidEnum/GetEnumNullable) — leaving them would have opened the #549 direction the moment the runtime tightened: a blob the modal accepts and the driver skips. RED first: 10 failing in Core.Abstractions.Tests, 4 in Driver.Sql.Tests, and 10 of the 11 new fleet-wide parity cases (re-confirmed by reverting the Core fix).
This commit is contained in:
@@ -8,10 +8,13 @@ public enum JsonEnumRead
|
||||
/// <summary>The property is absent, or present but not a JSON string (nothing to validate).</summary>
|
||||
Absent,
|
||||
|
||||
/// <summary>The property is a JSON string that parses (case-insensitively) to the enum.</summary>
|
||||
/// <summary>The property is a JSON string naming (case-insensitively) a declared enum member.</summary>
|
||||
Valid,
|
||||
|
||||
/// <summary>The property is a JSON string that does NOT parse to the enum (a typo'd value).</summary>
|
||||
/// <summary>
|
||||
/// The property is a JSON string that names no declared enum member — a typo, or a numeric
|
||||
/// spelling such as <c>"8"</c>, which is not a name at all.
|
||||
/// </summary>
|
||||
Invalid,
|
||||
}
|
||||
|
||||
@@ -67,6 +70,19 @@ public static class TagConfigJson
|
||||
/// treatment of an absent or present-but-non-string property — so a caller that extracts "the
|
||||
/// string value, or null" gets the runtime's leniency for free.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Only a member NAME is valid — a numeric spelling is refused before the parse.</b>
|
||||
/// <see cref="Enum.TryParse{TEnum}(string, bool, out TEnum)"/> accepts <c>"1"</c> for any enum
|
||||
/// and hands back whatever member sits at that ordinal, and an out-of-range <c>"99"</c> parses
|
||||
/// just as readily — so a TagConfig carrying a numerically-serialised enum (the repo-wide
|
||||
/// authoring trap) read <see cref="JsonEnumRead.Valid"/> at all six driver parsers and bound a
|
||||
/// tag to a type nobody named, under <c>Good</c> quality. The same idiom guards
|
||||
/// <c>RawBrowseCommitMapper.NativeType</c> and <c>MTConnectTagDefinitionFactory</c>'s own type
|
||||
/// parse. <see cref="Enum.IsDefined{TEnum}(TEnum)"/> then covers what the leading-character
|
||||
/// check cannot: <c>TryParse</c> also bitwise-ORs a comma-separated list for a
|
||||
/// non-<c>[Flags]</c> enum, so <c>"Beta, Gamma"</c> would otherwise parse to a value no member
|
||||
/// declares.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <typeparam name="TEnum">The enum type to parse.</typeparam>
|
||||
/// <param name="text">The authored value's text, or <see langword="null"/> when there is none.</param>
|
||||
@@ -80,12 +96,26 @@ public static class TagConfigJson
|
||||
value = default;
|
||||
return JsonEnumRead.Absent;
|
||||
}
|
||||
if (Enum.TryParse(text, ignoreCase: true, out value))
|
||||
if (IsMemberNameSpelling(text) && Enum.TryParse(text, ignoreCase: true, out value) && Enum.IsDefined(value))
|
||||
return JsonEnumRead.Valid;
|
||||
value = default;
|
||||
return JsonEnumRead.Invalid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether <paramref name="text"/> is spelled as a member name rather than as a number. Judged on
|
||||
/// the TRIMMED text because <see cref="Enum.TryParse{TEnum}(string, bool, out TEnum)"/> trims
|
||||
/// before parsing, so <c>" 8 "</c> is the numeric spelling too; a C# member name can never begin
|
||||
/// with a digit or a sign, so no valid name is lost.
|
||||
/// </summary>
|
||||
/// <param name="text">The authored value's text.</param>
|
||||
/// <returns><see langword="true"/> when the text could be a member name.</returns>
|
||||
private static bool IsMemberNameSpelling(string text)
|
||||
{
|
||||
var span = text.AsSpan().Trim();
|
||||
return span.Length > 0 && !char.IsAsciiDigit(span[0]) && span[0] is not ('-' or '+');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lenient enum read with today's exact semantics: absent OR invalid ⇒ <paramref name="fallback"/>;
|
||||
/// only a present, parseable string yields the parsed value. Bit-for-bit equivalent to the six
|
||||
|
||||
Reference in New Issue
Block a user