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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ format changes.

### Added

- **The lite tier reads and writes fields of `google.protobuf.Any`, `Empty`, `FieldMask`, `Struct`, `Value`, `ListValue` and `NullValue`** (trendvidia/protowire#539). A lite schema with such a field did not compile, because its generated meta named `com.google.protobuf.AnyPxfMeta`, which nothing generates. `:pxf-runtime` now ships these companions in `org.protowire.pxf.wkt`, as `protoc-gen-pxf-java-meta`'s output for the four google files, and the plugin from trendvidia/protowire#550 on points there. They are written as ordinary blocks, the same text protowire-go writes. An Any is written as `type_url` and `value`, since the lite tier has no type resolver. `pxf-runtime/scripts/regen-wkt-companions.sh` regenerates the companions.

- **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.
- **`Convert.xmlToProto` reads a `char` type's `characterEncoding`** (#173; draft `-01` § Character Encoding, protowire#494). A `char` becomes a `string` only when the encoding is absent (the FIX SBE default, US-ASCII) or names US-ASCII or UTF-8, by IANA name or alias, ignoring case and punctuation. Any other encoding becomes `bytes` of the same `(sbe.length)`, with a comment that says why. Before, every `char` was a `string`, so a schema declaring `ISO-8859-1` imported cleanly, and the first byte outside UTF-8 then failed the whole message. This follows protowire-go#248.
- **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.
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=474e8ece24eb48f4f840db7d68fd87eb67a90a89
pxfJavaMeta.ref=9e5474019dca0b5573e61f3ac35d88334b38eb91
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 TrendVidia, LLC.
syntax = "proto3";
package wkt.test.v1;

option java_package = "wkt.test.v1";
option java_multiple_files = true;

import "google/protobuf/any.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";
import "google/protobuf/struct.proto";

// The google/protobuf types PXF reads as ordinary messages. Their companions
// ship in org.protowire.pxf.wkt; before trendvidia/protowire#539 the generated
// meta named com.google.protobuf.AnyPxfMeta, which does not exist, and this
// file did not compile.
message GoogleMessages {
google.protobuf.Any any = 1;
repeated google.protobuf.Any anys = 2;
map<string, google.protobuf.Any> any_map = 3;
google.protobuf.Empty empty = 4;
google.protobuf.FieldMask mask = 5;
google.protobuf.Struct struct = 6;
google.protobuf.Value value = 7;
google.protobuf.ListValue list = 8;
google.protobuf.NullValue null_value = 9;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 TrendVidia, LLC.
package org.protowire.lite.it;

import com.google.protobuf.Any;
import com.google.protobuf.ByteString;
import com.google.protobuf.Empty;
import com.google.protobuf.FieldMask;
import com.google.protobuf.ListValue;
import com.google.protobuf.NullValue;
import com.google.protobuf.Struct;
import com.google.protobuf.Value;
import org.junit.jupiter.api.Test;
import wkt.test.v1.GoogleMessages;
import wkt.test.v1.GoogleMessagesPxfCodec;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* A lite schema with a field of each google/protobuf type that PXF reads as an
* ordinary message compiles, and each round-trips through the generated codec
* with no registry (trendvidia/protowire#539). The lite tier has no type
* resolver, so an Any is written as {@code type_url} and {@code value}, which
* protowire#501 allows: the bytes read back are the stored bytes.
*/
final class GoogleMessagesLiteTest {

private static GoogleMessages roundTrip(GoogleMessages in) {
String text = GoogleMessagesPxfCodec.marshal(in);
GoogleMessages back = GoogleMessagesPxfCodec.unmarshal(text);
assertEquals(in, back, text);
return back;
}

/**
* Bytes no body could reproduce: an unknown field 99, then field 1 twice.
* Read back as anything but these bytes, the Any has changed.
*/
private static final ByteString ODD_VALUE = ByteString.copyFrom(new byte[] {
(byte) 0x98, 0x06, 0x01, 0x08, 0x01, 0x08, 0x02,
});

@Test
void anAnyKeepsItsTypeUrlAndValueBytes() {
Any any = Any.newBuilder().setTypeUrl("type.googleapis.com/x.v1.Thing").setValue(ODD_VALUE).build();
GoogleMessages back = roundTrip(GoogleMessages.newBuilder()
.setAny(any)
.addAnys(any)
.addAnys(Any.getDefaultInstance())
.putAnyMap("k", any)
.build());
assertEquals(ODD_VALUE, back.getAny().getValue());
assertEquals(ODD_VALUE, back.getAnys(0).getValue());
assertEquals(ODD_VALUE, back.getAnyMapOrThrow("k").getValue());
}

@Test
void anAnyIsWrittenAsItsTwoFields() {
String text = GoogleMessagesPxfCodec.marshal(GoogleMessages.newBuilder()
.setAny(Any.newBuilder().setTypeUrl("t/x").setValue(ODD_VALUE))
.build());
assertTrue(text.contains("type_url = \"t/x\""), text);
assertTrue(text.contains("value = b\""), text);
}

@Test
void emptyAndFieldMask() {
roundTrip(GoogleMessages.newBuilder()
.setEmpty(Empty.getDefaultInstance())
.setMask(FieldMask.newBuilder().addPaths("a.b").addPaths("c"))
.build());
}

@Test
void structValueAndListValue() {
Value nested = Value.newBuilder().setListValue(ListValue.newBuilder()
.addValues(Value.newBuilder().setNumberValue(1.5))
.addValues(Value.newBuilder().setStringValue("s"))
.addValues(Value.newBuilder().setBoolValue(true))
.addValues(Value.newBuilder().setNullValue(NullValue.NULL_VALUE)))
.build();
Struct struct = Struct.newBuilder()
.putFields("n", Value.newBuilder().setNumberValue(2).build())
.putFields("l", nested)
.putFields("s", Value.newBuilder().setStructValue(Struct.newBuilder()
.putFields("inner", Value.newBuilder().setStringValue("x").build())).build())
.build();
roundTrip(GoogleMessages.newBuilder()
.setStruct(struct)
.setValue(Value.newBuilder().setStructValue(struct))
.setList(nested.getListValue())
.build());
}

@Test
void aNullValueEnumField() {
// NULL_VALUE is 0, a plain enum's zero, so it is not written; the
// field reads back as its zero either way.
roundTrip(GoogleMessages.newBuilder().setNullValue(NullValue.NULL_VALUE).build());
// A Value whose kind is null_value has presence, so it is written.
GoogleMessages back = roundTrip(GoogleMessages.newBuilder()
.setValue(Value.newBuilder().setNullValue(NullValue.NULL_VALUE)).build());
assertEquals(Value.KindCase.NULL_VALUE, back.getValue().getKindCase());
}
}
42 changes: 42 additions & 0 deletions pxf-runtime/scripts/regen-wkt-companions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 TrendVidia, LLC.
#
# Regenerates org.protowire.pxf.wkt: the PxfMeta and PxfEnum companions of the
# google/protobuf types that PXF reads as ordinary messages (Any, Empty,
# FieldMask, Struct, Value, ListValue, NullValue). protoc-gen-pxf-java-meta
# points a field of one of these types here, since no build runs it over the
# google files themselves (trendvidia/protowire#539). The files are the
# plugin's own output for those files, with only the package line changed.
#
# Usage: regen-wkt-companions.sh <protowire checkout> [--check]
# --check fail if the checked-in files differ from fresh output.
set -euo pipefail

spec=${1:?usage: $0 <protowire checkout> [--check]}
mode=${2:-}
here=$(cd "$(dirname "$0")/.." && pwd)
dest=$here/src/main/java/org/protowire/pxf/wkt
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT

(cd "$spec" && go build -o "$work/protoc-gen-pxf-java-meta" ./cmd/protoc-gen-pxf-java-meta)
mkdir -p "$work/out"
protoc --plugin=protoc-gen-pxf-java-meta="$work/protoc-gen-pxf-java-meta" \
--pxf-java-meta_out="$work/out" \
google/protobuf/any.proto google/protobuf/empty.proto \
google/protobuf/field_mask.proto google/protobuf/struct.proto

mkdir -p "$work/wkt"
for f in "$work"/out/com/google/protobuf/*.java; do
sed 's/^package com\.google\.protobuf;$/package org.protowire.pxf.wkt;/' "$f" > "$work/wkt/$(basename "$f")"
done

if [[ $mode == --check ]]; then
diff -r "$work/wkt" "$dest"
echo "org.protowire.pxf.wkt matches the plugin's output"
else
rm -rf "$dest" && mkdir -p "$dest"
cp "$work"/wkt/*.java "$dest/"
ls "$dest"
fi
131 changes: 131 additions & 0 deletions pxf-runtime/src/main/java/org/protowire/pxf/wkt/AnyPxfMeta.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Generated by protoc-gen-pxf-java-meta. DO NOT EDIT.
// source: google/protobuf/any.proto

package org.protowire.pxf.wkt;

import java.util.Map;
import java.util.Set;

import org.protowire.pxf.PxfEnum;
import org.protowire.pxf.PxfMeta;

/**
* PXF / SBE descriptor metadata for {@code Any}, lifted at
* codegen time so the runtime never needs descriptor reflection.
*
* <p>Implements {@link PxfMeta} so the protobuf-javalite runtime can dispatch
* through the interface — pass {@code AnyPxfMeta.INSTANCE} where a {@code PxfMeta}
* is expected. The static {@code FIELD_NUMBERS}, {@code FIELD_KINDS}, etc.
* fields remain public for direct access by static-only consumers.
*/
public final class AnyPxfMeta implements PxfMeta {
private AnyPxfMeta() {}

/** Singleton; pass to runtime APIs that take a {@link PxfMeta}. */
public static final AnyPxfMeta INSTANCE = new AnyPxfMeta();

/** Field name → field number, in declaration order. */
public static final Map<String, Integer> FIELD_NUMBERS = Map.ofEntries(
Map.entry("type_url", 1),
Map.entry("value", 2)
);

/**
* Field number → proto kind integer. Values follow
* {@code google.protobuf.FieldDescriptorProto.Type} (1..18, e.g. 9 = STRING,
* 11 = MESSAGE, 14 = ENUM).
*/
public static final Map<Integer, Integer> FIELD_KINDS = Map.ofEntries(
Map.entry(1, 9),
Map.entry(2, 12)
);

/** Field number → wire type integer (0=varint, 1=i64, 2=length-delimited, 5=i32). */
public static final Map<Integer, Integer> WIRE_TYPES = Map.ofEntries(
Map.entry(1, 2),
Map.entry(2, 2)
);

/** Field numbers matching the predicate (see Javadoc on each constant). */
public static final Set<Integer> REPEATED_FIELDS = Set.of();

/** Field numbers matching the predicate (see Javadoc on each constant). */
public static final Set<Integer> PACKED_FIELDS = Set.of();

/** Field number → fully-qualified Java class name, for fields of the matching kind. */
public static final Map<Integer, String> MESSAGE_TYPES = Map.of();

/** Field number → fully-qualified Java class name, for fields of the matching kind. */
public static final Map<Integer, String> ENUM_TYPES = Map.of();

/** Field numbers marked {@code [(pxf.required) = true]} or {@code @required}. */
public static final Set<Integer> REQUIRED_FIELDS = Set.of();

/**
* Field number → {@code (pxf.default)} or {@code @default} literal. Values are PXF text
* literals (e.g. {@code "42"}, {@code "true"}, {@code "\"hi\""}); the
* runtime feeds them through the PXF lexer to resolve to typed values.
*/
public static final Map<Integer, String> DEFAULTS = Map.of();

/** Field numbers matching the predicate (see Javadoc on each constant). */
public static final Set<Integer> NULLABLE_FIELDS = Set.of();

/** Field numbers matching the predicate (see Javadoc on each constant). */
public static final Set<Integer> IMPLICIT_PRESENCE_FIELDS = Set.of(1, 2);

/** Field number → {@code (pxf.key)} key field name, for keyed repeated fields. */
public static final Map<Integer, String> KEY_FIELDS = Map.of();

/** {@code (sbe.template_id)} value, or -1 if unset. */
public static final int SBE_TEMPLATE_ID = -1;

/** Field number → {@code (sbe.length)} byte length, where set. */
public static final Map<Integer, Integer> SBE_FIELD_LENGTHS = Map.of();

/** Field number → {@code (sbe.encoding)} primitive name, where set. */
public static final Map<Integer, String> SBE_FIELD_ENCODINGS = Map.of();

/**
* Field number → declaring oneof name, for fields inside a user-declared
* {@code oneof} block. Synthetic oneofs around proto3 {@code optional}
* fields are excluded.
*/
public static final Map<Integer, String> ONEOF_OF = Map.of();

/** Field numbers declared as {@code map<K, V>} in the source .proto. */
public static final Set<Integer> MAP_FIELDS = Set.of();

/**
* Field number → {@code PxfMeta.WKT_*} kind constant for fields whose
* target message is a recognized well-known type.
*/
public static final Map<Integer, Integer> WELL_KNOWN_KINDS = Map.of();

/** Field number → nested {@link PxfMeta} INSTANCE for MESSAGE-kind fields. */
public static final Map<Integer, PxfMeta> NESTED_METAS = Map.of();

/** Field number → {@link PxfEnum} INSTANCE for ENUM-kind fields. */
public static final Map<Integer, PxfEnum> ENUM_METAS = Map.of();

@Override public Map<String, Integer> fieldNumbers() { return FIELD_NUMBERS; }
@Override public Map<Integer, Integer> fieldKinds() { return FIELD_KINDS; }
@Override public Map<Integer, Integer> wireTypes() { return WIRE_TYPES; }
@Override public Set<Integer> repeatedFields() { return REPEATED_FIELDS; }
@Override public Set<Integer> packedFields() { return PACKED_FIELDS; }
@Override public Map<Integer, String> messageTypes() { return MESSAGE_TYPES; }
@Override public Map<Integer, String> enumTypes() { return ENUM_TYPES; }
@Override public Set<Integer> requiredFields() { return REQUIRED_FIELDS; }
@Override public Map<Integer, String> defaults() { return DEFAULTS; }
public Set<Integer> nullableFields() { return NULLABLE_FIELDS; }
public Set<Integer> implicitPresenceFields() { return IMPLICIT_PRESENCE_FIELDS; }
@Override public int sbeTemplateId() { return SBE_TEMPLATE_ID; }
@Override public Map<Integer, Integer> sbeFieldLengths() { return SBE_FIELD_LENGTHS; }
@Override public Map<Integer, String> sbeFieldEncodings() { return SBE_FIELD_ENCODINGS; }
@Override public Map<Integer, String> oneofOf() { return ONEOF_OF; }
@Override public String fullName() { return "google.protobuf.Any"; }
@Override public Map<Integer, PxfMeta> nestedMetas() { return NESTED_METAS; }
@Override public Map<Integer, PxfEnum> enumMetas() { return ENUM_METAS; }
@Override public Set<Integer> mapFields() { return MAP_FIELDS; }
@Override public Map<Integer, Integer> wellKnownKinds() { return WELL_KNOWN_KINDS; }
}
Loading
Loading