fix(ablegacy): refuse libplctag writes to translated status bits (#659 fix round 1)
The read translation R6:0.DN -> R6:0.CON/13 made those names parse in libplctag, which would have turned a write - previously impossible, tag creation failed - into a masked write to the control word. LibplctagLegacyTagRuntime.WriteRefusal (via IAbLegacyTagFactory.WriteRefusal, same plumbing as native) now refuses writes to every rewritten status bit (R EN/EU/DN/EM/ER/UL/IN/FD, C UA) with BadNotSupported before a runtime is created; the runtime also refuses as defence in depth. Reads stay translated. T/C status bits libplctag parses itself keep their pre-existing behaviour. 13 new tests fail with the predicate disabled. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -43,14 +43,27 @@ internal static class LibplctagLegacyName
|
||||
/// <summary>Returns the libplctag tag name for a canonical AbLegacy address.</summary>
|
||||
/// <param name="canonical">The canonical (decimal-normalised) address, e.g. <c>S:0</c> or <c>R6:0.DN</c>.</param>
|
||||
/// <returns>The libplctag <c>name=</c> value, e.g. <c>S2:0</c> or <c>R6:0.CON/13</c>.</returns>
|
||||
public static string Translate(string canonical)
|
||||
public static string Translate(string canonical) => TranslateCore(canonical).Name;
|
||||
|
||||
/// <summary>
|
||||
/// True when <paramref name="canonical"/> is a status bit this translator rewrites to control word + bit
|
||||
/// (<c>R6:0.DN</c> → <c>R6:0.CON/13</c>; R <c>EN/EU/DN/EM/ER/UL/IN/FD</c>, counter <c>UA</c>). A WRITE to one is
|
||||
/// refused (<see cref="LibplctagLegacyTagRuntime.WriteRefusal"/>): before the translation libplctag could not
|
||||
/// even create such a tag, so a write never reached the PLC, and the rewrite must not turn it into a new masked
|
||||
/// write to a production controller's control word (the native transport refuses status-bit writes too).
|
||||
/// </summary>
|
||||
/// <param name="canonical">The canonical address.</param>
|
||||
/// <returns><see langword="true"/> for a rewritten status bit.</returns>
|
||||
public static bool IsRewrittenStatusBit(string canonical) => TranslateCore(canonical).RewrittenStatusBit;
|
||||
|
||||
private static (string Name, bool RewrittenStatusBit) TranslateCore(string canonical)
|
||||
{
|
||||
// Family-neutral parse is correct: the driver hands the runtime AbLegacyAddress.ToLibplctagName(), already
|
||||
// decimal-normalised (PLC-5 octal I/O included) — the same reasoning as NativeAbLegacyTagRuntime.ToPcccAddress.
|
||||
if (AbLegacyAddress.TryParse(canonical) is not { IsIndirect: false } a) return canonical;
|
||||
if (AbLegacyAddress.TryParse(canonical) is not { IsIndirect: false } a) return (canonical, false);
|
||||
|
||||
var fileNumber = a.FileNumber ?? PcccFileType.ImplicitFileNumber(a.FileLetter);
|
||||
if (fileNumber is null) return canonical;
|
||||
if (fileNumber is null) return (canonical, false);
|
||||
|
||||
var name = $"{a.FileLetter}{fileNumber}:{a.WordNumber}";
|
||||
if (a.SubElement is { } sub)
|
||||
@@ -68,16 +81,16 @@ internal static class LibplctagLegacyName
|
||||
}
|
||||
catch (AbTransportException)
|
||||
{
|
||||
return canonical; // a mnemonic neither transport knows; libplctag rejects it as before
|
||||
return (canonical, false); // a mnemonic neither transport knows; libplctag rejects it as before
|
||||
}
|
||||
|
||||
// Only status bits live in the control word; a word sub-element libplctag lacks has no bit form.
|
||||
if (native is not { SubElement: 0, Bit: int bit }) return canonical;
|
||||
name += $".CON/{bit}";
|
||||
if (native is not { SubElement: 0, Bit: int bit }) return (canonical, false);
|
||||
return ($"{name}.CON/{bit}", true); // a sub-element never carries a /bit as well
|
||||
}
|
||||
}
|
||||
|
||||
if (a.BitIndex is int b) name += $"/{b}";
|
||||
return name;
|
||||
return (name, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
|
||||
{
|
||||
private readonly Tag _tag;
|
||||
|
||||
/// <summary>Why a write to this tag is refused before the wire (<see cref="WriteRefusal"/>), else null.</summary>
|
||||
private readonly string? _writeRefusal;
|
||||
|
||||
/// <summary>
|
||||
/// The status the native tag held when a failed initialisation destroyed it (see
|
||||
/// <see cref="LibplctagOrphanRelease"/>); null while the wrapper's own handle is authoritative.
|
||||
@@ -29,6 +32,7 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
|
||||
public LibplctagLegacyTagRuntime(AbLegacyTagCreateParams p)
|
||||
{
|
||||
_tag = BuildTag(p);
|
||||
_writeRefusal = WriteRefusal(p);
|
||||
}
|
||||
|
||||
/// <summary>Builds the (uninitialized) libplctag tag for <paramref name="p"/>.</summary>
|
||||
@@ -59,8 +63,27 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime
|
||||
RunReleasingOrphanAsync(() => _tag.ReadAsync(cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>Throws the <see cref="WriteRefusal"/> refusal (Unsupported) before anything is sent — defence in depth;
|
||||
/// the driver asks the factory first and never creates a runtime for a refused write.</remarks>
|
||||
public Task WriteAsync(CancellationToken cancellationToken) =>
|
||||
RunReleasingOrphanAsync(() => _tag.WriteAsync(cancellationToken));
|
||||
_writeRefusal is { } refusal
|
||||
? Task.FromException(new AbTransportException(AbStatus.Unsupported, refusal))
|
||||
: RunReleasingOrphanAsync(() => _tag.WriteAsync(cancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// The writes the libplctag runtime refuses before the wire, or <see langword="null"/>: a status bit that
|
||||
/// <see cref="LibplctagLegacyName"/> rewrites to control word + bit (<c>R6:0.DN</c> → <c>R6:0.CON/13</c>, R
|
||||
/// <c>EN/EU/DN/EM/ER/UL/IN/FD</c>, counter <c>UA</c>). Before #659 libplctag could not create those tags at all,
|
||||
/// so a write to one never reached the controller; the read-side translation must not open a new masked write
|
||||
/// to a control word. T/C status bits libplctag itself parses (<c>T4:0.DN</c>, <c>C5:0.CU</c>…) keep their
|
||||
/// pre-existing behaviour — a masked bit write (documented hazard in <c>docs/drivers/AbLegacy.md</c>).
|
||||
/// </summary>
|
||||
/// <param name="p">The tag's create parameters.</param>
|
||||
/// <returns>The refusal reason, or <see langword="null"/>.</returns>
|
||||
internal static string? WriteRefusal(AbLegacyTagCreateParams p) =>
|
||||
LibplctagLegacyName.IsRewrittenStatusBit(p.TagName)
|
||||
? $"Writes to the status bit '{p.TagName}' are not supported on the libplctag transport (libplctag has no mnemonic for it; it is read as control word + bit)."
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
/// Runs a libplctag operation; when it fails and leaves the tag uninitialised, destroys the native tag it
|
||||
@@ -236,4 +259,7 @@ internal sealed class LibplctagLegacyTagFactory : IAbLegacyTagFactory
|
||||
/// <inheritdoc />
|
||||
public IAbLegacyTagRuntime Create(AbLegacyTagCreateParams createParams) =>
|
||||
new LibplctagLegacyTagRuntime(createParams);
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? WriteRefusal(AbLegacyTagCreateParams createParams) => LibplctagLegacyTagRuntime.WriteRefusal(createParams);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user