fix(driver-ablegacy): resolve High code-review findings (Driver.AbLegacy-001, Driver.AbLegacy-006)
Driver.AbLegacy-001 — PCCC bit-index range. AbLegacyAddress.TryParse accepted a bit index of 0..31 for every file type, but a 16-bit N/B/I/O/S/A word only has bits 0..15. TryParse now range-checks the bit index against the file's word width (0..15 for 16-bit element files, 0..31 for the 32-bit L file, no bits on float files), so addresses like N7:0/20 are rejected at parse time instead of silently truncating in the (short) cast. WriteBitInWordAsync reads and writes an L-file parent word as 32-bit Long and masks the RMW arithmetic to the native width, so a sign-extended 16-bit decode can no longer corrupt the high bits. Driver.AbLegacy-006 — shared-runtime concurrency. A per-tag libplctag Tag handle is cached and reused by both the server read path and the poll loop, with no synchronisation around Read/GetStatus/DecodeValue. Added a per-runtime SemaphoreSlim (DeviceState.GetRuntimeLock, keyed by tag name); ReadAsync and WriteAsync now hold it across the whole Read -> GetStatus -> Decode / Encode -> Write -> GetStatus sequence so no two threads touch the same Tag handle concurrently. Added xUnit + Shouldly regression coverage: AbLegacyBitIndexRangeTests (per-file bit-range validation + L-file 32-bit RMW + sign-extension safety) and AbLegacyRuntimeConcurrencyTests (overlap-detecting fake proving concurrent read/read and read/write are serialised). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -46,12 +46,14 @@ public sealed record AbLegacyAddress(
|
||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||
var src = value.Trim();
|
||||
|
||||
// BitIndex: trailing /N
|
||||
// BitIndex: trailing /N. The valid range depends on the parent word width, which is
|
||||
// determined by the file letter (16-bit N/B/I/O/S/A → 0..15, 32-bit L → 0..31). Capture
|
||||
// the raw value here and range-check it once the file letter is known (see below).
|
||||
int? bitIndex = null;
|
||||
var slashIdx = src.IndexOf('/');
|
||||
if (slashIdx >= 0)
|
||||
{
|
||||
if (!int.TryParse(src[(slashIdx + 1)..], out var bit) || bit < 0 || bit > 31) return null;
|
||||
if (!int.TryParse(src[(slashIdx + 1)..], out var bit) || bit < 0) return null;
|
||||
bitIndex = bit;
|
||||
src = src[..slashIdx];
|
||||
}
|
||||
@@ -91,9 +93,30 @@ public sealed record AbLegacyAddress(
|
||||
// Reject unknown file letters — these cover SLC/ML/PLC-5 canonical families.
|
||||
if (!IsKnownFileLetter(letter)) return null;
|
||||
|
||||
// Range-check the bit index against the file's word width. A PCCC N/B/I/O/S/A word is a
|
||||
// 16-bit element, so valid bit indices are 0..15; an L-file element is 32-bit (0..31).
|
||||
// F-files are 32-bit IEEE-754 floats and are not bit-addressable at all.
|
||||
if (bitIndex is int b)
|
||||
{
|
||||
var maxBit = MaxBitIndexFor(letter);
|
||||
if (maxBit < 0 || b > maxBit) return null;
|
||||
}
|
||||
|
||||
return new AbLegacyAddress(letter, fileNumber, word, bitIndex, subElement);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Highest valid bit index for a file letter, or <c>-1</c> if the file type is not
|
||||
/// bit-addressable. 16-bit element files (N/B/I/O/S/A) permit bits 0..15; the 32-bit
|
||||
/// L-file permits 0..31.
|
||||
/// </summary>
|
||||
private static int MaxBitIndexFor(string letter) => letter switch
|
||||
{
|
||||
"L" => 31,
|
||||
"N" or "B" or "I" or "O" or "S" or "A" => 15,
|
||||
_ => -1,
|
||||
};
|
||||
|
||||
private static bool IsKnownFileLetter(string letter) => letter switch
|
||||
{
|
||||
"N" or "F" or "B" or "L" or "ST" or "T" or "C" or "R" or "I" or "O" or "S" or "A" => true,
|
||||
|
||||
Reference in New Issue
Block a user