Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<Message>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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Message>SbeCodec, under the row's limits
package org.protowire.check.android;

import org.protowire.pxf.DecodeLimits;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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());
}
}
Loading