fix(abcip): controller browse honours the authored EtherNet/IP port (#539)

The two discovery-path libplctag tags (@tags symbol-list enumerator, @udt
template reader) still passed the bare Gateway host, so a browse against a
device authored on a non-default port silently browsed whatever answers on
44818 — #534's defect surviving in its discovery half. Both sites now consume
AbCipTagCreateParams.LibplctagGateway, with the Tag construction extracted
into internal static builders so the seam is unit-assertable (the #534 tests
pinned the property's composition but nothing pinned each site's consumption,
which is exactly how these two survived).

New guard: LibplctagGatewayConsumptionGuardTests scans both AB drivers'
sources and fails on any libplctag Tag built from the bare Gateway, so a
future call site cannot reintroduce the silent 44818 redirect.

Live-gated against the fixture host: browse of ab://10.100.0.35:44818
returns the nine-tag symbol table; browse of ab://10.100.0.35:44999 (nothing
listening — the issue's exact repro) now fails ErrorTimeout where it
previously returned the 44818 fixture's table.

Closes #539.

Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1
This commit is contained in:
Joseph Doherty
2026-08-06 20:42:40 -04:00
parent d0ac6fc58c
commit 97a837c147
4 changed files with 137 additions and 20 deletions
@@ -26,17 +26,7 @@ internal sealed class LibplctagTagEnumerator : IAbCipTagEnumerator
AbCipTagCreateParams deviceParams,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
// Build a tag specifically for the @tags pseudo — same gateway + path as the device,
// distinguished by the name alone.
_tag = new Tag
{
Gateway = deviceParams.Gateway,
Path = deviceParams.CipPath,
PlcType = MapPlcType(deviceParams.LibplctagPlcAttribute),
Protocol = Protocol.ab_eip,
Name = "@tags",
Timeout = deviceParams.Timeout,
};
_tag = BuildSymbolListTag(deviceParams);
await _tag.InitializeAsync(cancellationToken).ConfigureAwait(false);
await _tag.ReadAsync(cancellationToken).ConfigureAwait(false);
@@ -49,6 +39,26 @@ internal sealed class LibplctagTagEnumerator : IAbCipTagEnumerator
yield return tag;
}
/// <summary>
/// Builds the (uninitialized) libplctag tag for the <c>@tags</c> pseudo — same gateway +
/// path as the device, distinguished by the name alone.
/// </summary>
/// <remarks>
/// Gateway must be <see cref="AbCipTagCreateParams.LibplctagGateway"/> (host:port), never the
/// bare <see cref="AbCipTagCreateParams.Gateway"/> — libplctag substitutes 44818 for a bare
/// host, so a browse against a device authored on another port silently returns whatever
/// answers on 44818 (Gitea #539, the discovery half of #534).
/// </remarks>
internal static Tag BuildSymbolListTag(AbCipTagCreateParams deviceParams) => new()
{
Gateway = deviceParams.LibplctagGateway,
Path = deviceParams.CipPath,
PlcType = MapPlcType(deviceParams.LibplctagPlcAttribute),
Protocol = Protocol.ab_eip,
Name = "@tags",
Timeout = deviceParams.Timeout,
};
/// <summary>Disposes the enumerator and releases the underlying libplctag tag.</summary>
public void Dispose() => _tag?.Dispose();
@@ -19,20 +19,29 @@ internal sealed class LibplctagTemplateReader : IAbCipTemplateReader
CancellationToken cancellationToken)
{
_tag?.Dispose();
_tag = new Tag
{
Gateway = deviceParams.Gateway,
Path = deviceParams.CipPath,
PlcType = MapPlcType(deviceParams.LibplctagPlcAttribute),
Protocol = Protocol.ab_eip,
Name = $"@udt/{templateInstanceId}",
Timeout = deviceParams.Timeout,
};
_tag = BuildTemplateTag(deviceParams, templateInstanceId);
await _tag.InitializeAsync(cancellationToken).ConfigureAwait(false);
await _tag.ReadAsync(cancellationToken).ConfigureAwait(false);
return _tag.GetBuffer();
}
/// <summary>Builds the (uninitialized) libplctag tag for the <c>@udt/{id}</c> pseudo.</summary>
/// <remarks>
/// Gateway must be <see cref="AbCipTagCreateParams.LibplctagGateway"/> (host:port), never the
/// bare <see cref="AbCipTagCreateParams.Gateway"/> — libplctag substitutes 44818 for a bare
/// host, so a browse against a device authored on another port silently returns whatever
/// answers on 44818 (Gitea #539, the discovery half of #534).
/// </remarks>
internal static Tag BuildTemplateTag(AbCipTagCreateParams deviceParams, uint templateInstanceId) => new()
{
Gateway = deviceParams.LibplctagGateway,
Path = deviceParams.CipPath,
PlcType = MapPlcType(deviceParams.LibplctagPlcAttribute),
Protocol = Protocol.ab_eip,
Name = $"@udt/{templateInstanceId}",
Timeout = deviceParams.Timeout,
};
/// <summary>Disposes the template reader and releases the underlying libplctag tag.</summary>
public void Dispose() => _tag?.Dispose();