fix(clients): resolve 2026-06-18 array-write review findings
- Client.Dotnet-030: add advise-supervisory to IsKnownGatewayCommand (was dead/unreachable, exit 2) - Client.Go-035/036/037: usage+README list advise-supervisory; add session-id guard test; fix write2 README wording - Client.Python-037/038: drop regressed 'scaffold' from pyproject; add advise-supervisory CLI tests - Client.Rust-039/040: document write_array_elements/advise-supervisory in design doc; pin outer MxValue data_type==0 - Client.Java-049/050/051: sync CLIENT_VERSION to 0.1.2; add advise-supervisory test; guard negative uint32 inputs (pending windev gradle verification) Client READMEs also updated for Server-057 add-family normalization wording. .NET/Go/Python/Rust verified green locally; Java pending windev.
This commit is contained in:
@@ -144,8 +144,9 @@ array before forwarding to the worker. Indices not listed in the map are
|
||||
written as the element type's default — this is a **reset**, not a preserve;
|
||||
current values at those positions are discarded. `totalLength` is required and
|
||||
must match the declared length of the array attribute. Bare-name array items
|
||||
(`Area001.Pump001.Speed`) are auto-normalized to the `[]` form at `AddItem` so
|
||||
the array attribute accepts the write.
|
||||
(`Area001.Pump001.Speed`) are auto-normalized to the `[]` form across the whole
|
||||
add family — `AddItem`, `AddItem2`, `AddItemBulk`, and `AddBufferedItem` — so the
|
||||
array attribute accepts the write.
|
||||
|
||||
## Galaxy Repository Browse
|
||||
|
||||
@@ -396,7 +397,7 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.zb.mom.ww.mxgateway:zb-mom-ww-mxgateway-client:0.1.1'
|
||||
implementation 'com.zb.mom.ww.mxgateway:zb-mom-ww-mxgateway-client:0.1.2'
|
||||
}
|
||||
````
|
||||
|
||||
|
||||
+24
-3
@@ -56,7 +56,7 @@ final class MxGatewayCliTests {
|
||||
|
||||
assertEquals(0, run.exitCode());
|
||||
assertEquals("", run.errors());
|
||||
assertTrue(run.output().contains("mxgateway-java 0.1.1"));
|
||||
assertTrue(run.output().contains("mxgateway-java 0.1.2"));
|
||||
assertTrue(run.output().contains("gatewayProtocolVersion=3"));
|
||||
assertTrue(run.output().contains("workerProtocolVersion=1"));
|
||||
}
|
||||
@@ -86,7 +86,7 @@ final class MxGatewayCliTests {
|
||||
CliRun run = execute(new FakeClientFactory(), "version", "--json");
|
||||
|
||||
assertEquals(0, run.exitCode());
|
||||
assertTrue(run.output().contains("\"clientVersion\":\"0.1.1\""));
|
||||
assertTrue(run.output().contains("\"clientVersion\":\"0.1.2\""));
|
||||
assertTrue(run.output().contains("\"gatewayProtocolVersion\":3"));
|
||||
}
|
||||
|
||||
@@ -148,6 +148,26 @@ final class MxGatewayCliTests {
|
||||
assertTrue(run.output().contains("\"itemHandle\":7"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void adviseSupervisoryCommandCallsAdviseSupervisoryRaw() {
|
||||
// Client.Java-050: dedicated test for advise-supervisory, using a
|
||||
// separate adviseSupervisoryCalled flag so it cannot be masked by the
|
||||
// plain advise path that shares adviseCalled.
|
||||
FakeClientFactory factory = new FakeClientFactory();
|
||||
CliRun run = execute(
|
||||
factory,
|
||||
"advise-supervisory",
|
||||
"--session-id", "session-cli",
|
||||
"--server-handle", "12",
|
||||
"--item-handle", "34",
|
||||
"--json");
|
||||
|
||||
assertEquals(0, run.exitCode());
|
||||
assertTrue(factory.client.session.adviseSupervisoryCalled);
|
||||
assertFalse(factory.client.session.adviseCalled, "plain advise must not be called");
|
||||
assertTrue(run.output().contains("\"kind\":\"MX_COMMAND_KIND_ADVISE_SUPERVISORY\""));
|
||||
}
|
||||
|
||||
// ---- ping subcommand (D4) ----
|
||||
|
||||
@Test
|
||||
@@ -1235,6 +1255,7 @@ final class MxGatewayCliTests {
|
||||
private boolean registerCalled;
|
||||
private boolean addItemCalled;
|
||||
private boolean adviseCalled;
|
||||
private boolean adviseSupervisoryCalled;
|
||||
private MxValue lastWriteValue;
|
||||
private String lastPingMessage;
|
||||
private long lastReadBulkTimeoutMs;
|
||||
@@ -1304,7 +1325,7 @@ final class MxGatewayCliTests {
|
||||
|
||||
@Override
|
||||
public MxCommandReply adviseSupervisoryRaw(int serverHandle, int itemHandle) {
|
||||
adviseCalled = true;
|
||||
adviseSupervisoryCalled = true;
|
||||
return MxCommandReply.newBuilder()
|
||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE_SUPERVISORY)
|
||||
.setProtocolStatus(ok())
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ package com.zb.mom.ww.mxgateway.client;
|
||||
public final class MxGatewayClientVersion {
|
||||
private static final int GATEWAY_PROTOCOL_VERSION = 3;
|
||||
private static final int WORKER_PROTOCOL_VERSION = 1;
|
||||
private static final String CLIENT_VERSION = "0.1.1";
|
||||
private static final String CLIENT_VERSION = "0.1.2";
|
||||
|
||||
private MxGatewayClientVersion() {
|
||||
}
|
||||
|
||||
+22
-3
@@ -623,13 +623,22 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
* supplied indices must be within {@code [0, totalLength)}. Elements are iterated in
|
||||
* ascending index order so the produced command is deterministic.
|
||||
*
|
||||
* <p>Because the proto fields {@code MxSparseArray.total_length} and
|
||||
* {@code MxSparseElement.index} are {@code uint32}, passing a negative Java {@code int}
|
||||
* would silently sign-extend to a large unsigned value on the wire. This method
|
||||
* therefore rejects negative {@code totalLength} and negative element indices with
|
||||
* {@link IllegalArgumentException} rather than allowing a hard-to-diagnose gateway error.
|
||||
*
|
||||
* @param serverHandle the {@code ServerHandle} owning the item
|
||||
* @param itemHandle the {@code ItemHandle} to write
|
||||
* @param elementDataType the {@link MxDataType} of the array's elements
|
||||
* @param totalLength the total length of the expanded array
|
||||
* @param elements the indices to write mapped to their scalar values; unmentioned
|
||||
* indices are reset to the element type default
|
||||
* @param totalLength the total length of the expanded array; must be > 0
|
||||
* @param elements the indices to write mapped to their scalar values; each index must
|
||||
* be in {@code [0, totalLength)}; unmentioned indices are reset to the element
|
||||
* type default
|
||||
* @param userId the MXAccess user id used for security checks
|
||||
* @throws IllegalArgumentException if {@code totalLength} is not positive, or if any
|
||||
* element index is negative or ≥ {@code totalLength}
|
||||
* @throws MxGatewayException on transport or protocol failure
|
||||
*/
|
||||
public void writeArrayElements(
|
||||
@@ -641,6 +650,16 @@ public final class MxGatewaySession implements AutoCloseable {
|
||||
int userId) {
|
||||
Objects.requireNonNull(elementDataType, "elementDataType");
|
||||
Objects.requireNonNull(elements, "elements");
|
||||
if (totalLength <= 0) {
|
||||
throw new IllegalArgumentException("totalLength must be > 0, got " + totalLength);
|
||||
}
|
||||
for (Map.Entry<Integer, MxValue> entry : elements.entrySet()) {
|
||||
int idx = entry.getKey();
|
||||
if (idx < 0 || idx >= totalLength) {
|
||||
throw new IllegalArgumentException(
|
||||
"element index " + idx + " is out of range [0, " + totalLength + ")");
|
||||
}
|
||||
}
|
||||
MxSparseArray.Builder sparse = MxSparseArray.newBuilder()
|
||||
.setElementDataType(elementDataType)
|
||||
.setTotalLength(totalLength);
|
||||
|
||||
+58
@@ -451,6 +451,64 @@ final class MxGatewayClientSessionTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeArrayElementsRejectsNonPositiveTotalLength() throws Exception {
|
||||
// Client.Java-051: negative/zero totalLength silently sign-extends to a
|
||||
// large uint32 on the wire; the client must reject it with
|
||||
// IllegalArgumentException before building the proto message (before any
|
||||
// network call is issued).
|
||||
try (InProcessGateway gateway = InProcessGateway.start(
|
||||
new TestGatewayService() {}, new AtomicReference<>());
|
||||
MxGatewayClient client = gateway.client("", Duration.ofSeconds(5))) {
|
||||
MxGatewaySession session = MxGatewaySession.forSessionId(client, "guard-session");
|
||||
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> session.writeArrayElements(
|
||||
1, 2, MxDataType.MX_DATA_TYPE_INTEGER, -1, Map.of(), 0),
|
||||
"negative totalLength must throw");
|
||||
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> session.writeArrayElements(
|
||||
1, 2, MxDataType.MX_DATA_TYPE_INTEGER, 0, Map.of(), 0),
|
||||
"zero totalLength must throw");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeArrayElementsRejectsOutOfRangeIndex() throws Exception {
|
||||
// Client.Java-051: a negative index silently sign-extends to a large
|
||||
// uint32 on the wire; an index >= totalLength exceeds the declared
|
||||
// array bounds. Both must be caught before the network call.
|
||||
try (InProcessGateway gateway = InProcessGateway.start(
|
||||
new TestGatewayService() {}, new AtomicReference<>());
|
||||
MxGatewayClient client = gateway.client("", Duration.ofSeconds(5))) {
|
||||
MxGatewaySession session = MxGatewaySession.forSessionId(client, "guard-session");
|
||||
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> session.writeArrayElements(
|
||||
1, 2, MxDataType.MX_DATA_TYPE_INTEGER, 5,
|
||||
Map.of(-1, MxValues.int32Value(7)), 0),
|
||||
"negative index must throw");
|
||||
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> session.writeArrayElements(
|
||||
1, 2, MxDataType.MX_DATA_TYPE_INTEGER, 5,
|
||||
Map.of(5, MxValues.int32Value(7)), 0),
|
||||
"index equal to totalLength must throw");
|
||||
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> session.writeArrayElements(
|
||||
1, 2, MxDataType.MX_DATA_TYPE_INTEGER, 5,
|
||||
Map.of(10, MxValues.int32Value(7)), 0),
|
||||
"index above totalLength must throw");
|
||||
}
|
||||
}
|
||||
|
||||
private static ProtocolStatus ok() {
|
||||
return ProtocolStatus.newBuilder()
|
||||
.setCode(ProtocolStatusCode.PROTOCOL_STATUS_CODE_OK)
|
||||
|
||||
Reference in New Issue
Block a user