diff --git a/CHANGELOG.md b/CHANGELOG.md index bca926f..d7ddae6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ format changes. ### Added - **Producers refuse a message nested past `MaxNestingDepth`** (#167; HARDENING.md § Recursion, protowire#502). `Pxf.marshal` counts each `{` and `[` it writes, as a decoder of the text counts them. `Pb.marshal` counts each submessage and map entry. Past the limit, `Pxf.marshal` throws `PxfException` and `Pb.marshal` throws `IOException`, both with the message `nesting depth exceeds MaxNestingDepth=100`. Before, they wrote a document that a reader at the same limit refuses, and a reference cycle in a `Pb` struct overflowed the stack. The limit is per call: `MarshalOptions.withMaxNestingDepth` in `pxf`, and the new `org.protowire.pb.MarshalOptions` with `Pb.marshal(Object, MarshalOptions)`. An Any whose body would go past the limit is still written, as `type_url` and `value`. `check-decode --format pb-render` now reads its input with the stock reader's limit raised, so the corpus's 101-level rows reach the producer. +- **A generated lite `SbeCodec` takes per-call limits** (#148): `unmarshal(data, maxMessageSize, maxRepeatedCount)` and the same with a builder. This needs `protoc-gen-pxf-java-meta` from trendvidia/protowire#541 on. `check-decode-android` now decodes SBE through the generated codec, so the security gate tests the entry point a caller uses, not only `SbeWireCodec` underneath it. - **`check-decode` and `check-decode-android` gain `--format pxf-pb`** (protowire#509), for the spec's new write gate. It reads a PXF document and writes the PB bytes on stdout. The full tier decodes with `unmarshalFull`, as `dump-envelope` does, and the harness sorts map entries by key before writing, because protobuf-java writes a `DynamicMessage`'s map entries in insertion order. On its first run the gate found 20 pairs that differ from protowire-go, filed as #156, #157 and #158. - **`PxfMeta.implicitPresenceFields()`** (#156): the singular fields with no presence, which a writer leaves out at their zero value. The default is empty. `protoc-gen-pxf-java-meta` emits `IMPLICIT_PRESENCE_FIELDS` from trendvidia/protowire#532 on, so a companion generated by an older plugin keeps writing every zero. diff --git a/check-decode-android/src/main/java/org/protowire/check/android/CheckDecodeAndroid.java b/check-decode-android/src/main/java/org/protowire/check/android/CheckDecodeAndroid.java index 17028fa..fd59f74 100644 --- a/check-decode-android/src/main/java/org/protowire/check/android/CheckDecodeAndroid.java +++ b/check-decode-android/src/main/java/org/protowire/check/android/CheckDecodeAndroid.java @@ -11,8 +11,7 @@ // pxf-pb as pxf, and writes the wire bytes on stdout // pb, pb-render wire to text, through LiteWireReader // pb-text as pb-render, and writes the text on stdout -// sbe SbeWireCodec on the generated template, called directly -// because the generated codec takes no limits +// sbe the generated SbeCodec, under the row's limits package org.protowire.check.android; import org.protowire.pxf.DecodeLimits; @@ -21,7 +20,6 @@ import org.protowire.pxf.PxfMeta; import org.protowire.pxf.android.LiteWireReader; import org.protowire.pxf.android.LiteWireWriter; -import org.protowire.sbe.runtime.SbeWireCodec; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -105,14 +103,14 @@ private static void run(String format, String schema, PxfMeta meta, String input } } + // Through the generated codec, so the gate proves the entry point a + // caller uses, not only the runtime under it (#148). private static void sbeDecode(byte[] data, String schema, DecodeLimits limits) { switch (schema) { - case "adversarial.v1.SbeWithGroup" -> SbeWireCodec.unmarshal(data, - adversarial.v1.SbeWithGroupSbeMeta.writer(adversarial.v1.SbeWithGroup.newBuilder()), - adversarial.v1.SbeWithGroupSbeMeta.TEMPLATE, limits.maxMessageSize(), limits.maxRepeatedCount()); - case "adversarial.v1.SbeText" -> SbeWireCodec.unmarshal(data, - adversarial.v1.SbeTextSbeMeta.writer(adversarial.v1.SbeText.newBuilder()), - adversarial.v1.SbeTextSbeMeta.TEMPLATE, limits.maxMessageSize(), limits.maxRepeatedCount()); + case "adversarial.v1.SbeWithGroup" -> adversarial.v1.SbeWithGroupSbeCodec.unmarshal( + data, limits.maxMessageSize(), limits.maxRepeatedCount()); + case "adversarial.v1.SbeText" -> adversarial.v1.SbeTextSbeCodec.unmarshal( + data, limits.maxMessageSize(), limits.maxRepeatedCount()); default -> throw new UnsupportedFormat("the lite tier has no SBE template for " + schema); } } diff --git a/gradle.properties b/gradle.properties index 451339a..aabc72e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,4 +12,4 @@ org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 # A release tag of trendvidia/protowire, or a full commit SHA while a plugin # change waits for its tag; bump when the plugin changes # (cmd/protoc-gen-pxf-java-meta). -pxfJavaMeta.ref=ee27eba68a624e3d8a598183e37eb51e46441cf8 +pxfJavaMeta.ref=474e8ece24eb48f4f840db7d68fd87eb67a90a89 diff --git a/sbe-android-codegen-it/src/test/java/org/protowire/sbe/it/SbeCodecLimitsTest.java b/sbe-android-codegen-it/src/test/java/org/protowire/sbe/it/SbeCodecLimitsTest.java new file mode 100644 index 0000000..5617244 --- /dev/null +++ b/sbe-android-codegen-it/src/test/java/org/protowire/sbe/it/SbeCodecLimitsTest.java @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 TrendVidia, LLC. +package org.protowire.sbe.it; + +import org.junit.jupiter.api.Test; +import org.protowire.sbe.testproto.NewOrderSingle; +import org.protowire.sbe.testproto.NewOrderSingleSbeCodec; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The generated codec hands per-call limits to the runtime (HARDENING.md + * § Mandatory limits, #148): MaxMessageSize on the input, MaxRepeatedCount + * on a group's declared count. + */ +final class SbeCodecLimitsTest { + + private static byte[] wire(int fills) { + NewOrderSingle.Builder b = NewOrderSingle.newBuilder().setOrderId(1).setSymbol("AAPL"); + for (int i = 0; i < fills; i++) b.addFills(NewOrderSingle.Fill.newBuilder().setFillId(i)); + return NewOrderSingleSbeCodec.marshal(b.build()); + } + + @Test + void maxMessageSizeIsTheCallers() { + byte[] data = wire(3); + assertEquals(3, NewOrderSingleSbeCodec.unmarshal(data, data.length, 3).getFillsCount()); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> NewOrderSingleSbeCodec.unmarshal(data, data.length - 1, 3)); + assertTrue(e.getMessage().contains("exceeds MaxMessageSize=" + (data.length - 1)), e.getMessage()); + } + + @Test + void maxRepeatedCountIsTheCallers() { + byte[] data = wire(3); + Exception e = assertThrows(RuntimeException.class, + () -> NewOrderSingleSbeCodec.unmarshal(data, NewOrderSingle.newBuilder(), data.length, 2)); + assertTrue(e.getMessage().contains("MaxRepeatedCount=2"), e.getMessage()); + } +}