Skip to content

Commit b98d4e2

Browse files
authored
Implement proto text format generator (#1080)
This syncs cl/816776257, not my code. Only text format generation is implemented. We don't parse the text format yet. (Related issue: #125)
1 parent 1ebb09c commit b98d4e2

File tree

11 files changed

+487
-15
lines changed

11 files changed

+487
-15
lines changed

protobuf/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
## 5.2.0
2+
3+
* New `GeneratedMessage` extension methods `toTextFormat` and `writeTextFormat`
4+
added to convert the message into the [official protocol buffers text
5+
format][text format]. ([#1080], [#125])
6+
7+
[text format]: https://protobuf.dev/reference/protobuf/textformat-spec/
8+
[#1080]: https://github.com/google/protobuf.dart/pull/1080
9+
[#125]: https://github.com/google/protobuf.dart/issues/125
10+
111
## 5.1.0
212

313
* Update default size limit of `CodedBufferReader` from 67,108,864 bytes to

protobuf/lib/src/protobuf/field_set.dart

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,100 @@ class FieldSet {
727727
}
728728
}
729729

730+
/// Writes the text proto string representation of the message.
731+
/// Spec: https://protobuf.dev/reference/protobuf/textformat-spec/
732+
void writeTextFormat(StringSink out) {
733+
_writeTextFormat(out, 0);
734+
}
735+
736+
void _writeTextFormat(StringSink out, int initialIndentLevel) {
737+
void writeIndent(int indentLevel) {
738+
for (var i = 0; i < indentLevel; i++) {
739+
out.writeCharCode(32);
740+
out.writeCharCode(32);
741+
}
742+
}
743+
744+
void renderValue(String key, dynamic value, int indentLevel) {
745+
writeIndent(indentLevel);
746+
if (value is GeneratedMessage) {
747+
out.write('$key {\n');
748+
value._fieldSet._writeTextFormat(out, indentLevel + 1);
749+
writeIndent(indentLevel);
750+
out.write('}\n');
751+
} else if (value is MapEntry) {
752+
out.write('$key {\n');
753+
renderValue('key', value.key, indentLevel + 1);
754+
renderValue('value', value.value, indentLevel + 1);
755+
writeIndent(indentLevel);
756+
out.write('}\n');
757+
} else if (value is String) {
758+
out.write('$key: "${escapeString(value)}"\n');
759+
} else if (value is Map) {
760+
out.write('$key {\n');
761+
for (final entry in value.entries) {
762+
renderValue(entry.key, entry.value, indentLevel + 1);
763+
}
764+
writeIndent(indentLevel);
765+
out.write('}\n');
766+
// Bytes are represented as a List<int> in Dart protobuf.
767+
} else if (value is List<int>) {
768+
out.write('$key: "');
769+
escapeBytes(value, out);
770+
out.write('"\n');
771+
} else {
772+
// Writes the primitive value as a string.
773+
out.write('$key: $value\n');
774+
}
775+
}
776+
777+
void writeFieldValue(String name, dynamic fieldValue) {
778+
if (fieldValue is PbList) {
779+
for (final value in fieldValue) {
780+
renderValue(name, value, initialIndentLevel);
781+
}
782+
} else if (fieldValue is PbMap) {
783+
for (final entry in fieldValue.entries) {
784+
renderValue(name, entry, initialIndentLevel);
785+
}
786+
} else {
787+
renderValue(name, fieldValue, initialIndentLevel);
788+
}
789+
}
790+
791+
for (final fi in _infosSortedByTag) {
792+
if (_hasField(fi.tagNumber)) {
793+
writeFieldValue(
794+
fi.name == '' ? fi.tagNumber.toString() : fi.protoName,
795+
_values[fi.index!],
796+
);
797+
}
798+
}
799+
800+
final extensions = _extensions;
801+
if (extensions != null) {
802+
extensions._info.keys.toList()
803+
..sort()
804+
..forEach((int tagNumber) {
805+
if (_hasField(tagNumber)) {
806+
writeFieldValue(
807+
'[${extensions._info[tagNumber]!.name}]',
808+
extensions._values[tagNumber],
809+
);
810+
}
811+
});
812+
}
813+
814+
_unknownFields?.writeTextFormat(out, initialIndentLevel);
815+
816+
final unknownJsonData = _unknownJsonData;
817+
if (unknownJsonData != null) {
818+
for (final entry in unknownJsonData.entries) {
819+
renderValue(entry.key, entry.value, initialIndentLevel);
820+
}
821+
}
822+
}
823+
730824
/// Merges the contents of the [other] into this message.
731825
///
732826
/// Singular fields that are set in [other] overwrite the corresponding fields

protobuf/lib/src/protobuf/generated_message.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,3 +616,21 @@ extension GeneratedMessageGenericExtensions<T extends GeneratedMessage> on T {
616616
extension GeneratedMessageInternalExtension on GeneratedMessage {
617617
FieldSet get fieldSet => _fieldSet;
618618
}
619+
620+
extension TextFormatExtension on GeneratedMessage {
621+
/// Returns a TextFormat [String] representation of this message.
622+
///
623+
/// Spec: https://protobuf.dev/reference/protobuf/textformat-spec/
624+
String toTextFormat() {
625+
final out = StringBuffer();
626+
writeTextFormat(out);
627+
return out.toString();
628+
}
629+
630+
/// Writes a TextFormat [String] representation of this message to [sink].
631+
///
632+
/// Spec: https://protobuf.dev/reference/protobuf/textformat-spec/
633+
void writeTextFormat(StringSink sink) {
634+
_fieldSet.writeTextFormat(sink);
635+
}
636+
}

protobuf/lib/src/protobuf/unknown_field_set.dart

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,70 @@ class UnknownFieldSet {
180180
return stringBuffer.toString();
181181
}
182182

183+
void writeTextFormat(StringSink out, int indentLevel) {
184+
for (final tag in sorted(_fields.keys)) {
185+
final field = _fields[tag]!;
186+
_writeUnknownFieldSetField(out, tag, field, indentLevel);
187+
}
188+
}
189+
190+
void _writeUnknownFieldSetField(
191+
StringSink out,
192+
int tag,
193+
UnknownFieldSetField field,
194+
int indentLevel,
195+
) {
196+
void writeIndent(StringSink out, int indentLevel) {
197+
for (var i = 0; i < indentLevel; i++) {
198+
out.writeCharCode(32);
199+
out.writeCharCode(32);
200+
}
201+
}
202+
203+
for (final value in field.varints) {
204+
writeIndent(out, indentLevel);
205+
out.write('$tag: ');
206+
final bi = value.toInt64();
207+
out.write(bi.toStringUnsigned());
208+
out.write('\n');
209+
}
210+
for (final value in field.fixed32s) {
211+
writeIndent(out, indentLevel);
212+
out.write(
213+
'$tag: 0x${value.toUnsigned(32).toRadixString(16).padLeft(8, '0')}\n',
214+
);
215+
}
216+
for (final value in field.fixed64s) {
217+
writeIndent(out, indentLevel);
218+
out.write('$tag: ');
219+
out.write('0x${value.toRadixStringUnsigned(16).padLeft(16, '0')}\n');
220+
}
221+
for (final value in field.lengthDelimited) {
222+
writeIndent(out, indentLevel);
223+
out.write('$tag: ');
224+
try {
225+
final ufs =
226+
UnknownFieldSet()
227+
..mergeFromCodedBufferReader(CodedBufferReader(value));
228+
out.write('{\n');
229+
ufs.writeTextFormat(out, indentLevel + 1);
230+
writeIndent(out, indentLevel);
231+
out.write('}\n');
232+
} on InvalidProtocolBufferException {
233+
out.write('"');
234+
escapeBytes(value, out);
235+
out.write('"\n');
236+
}
237+
}
238+
for (final value in field.groups) {
239+
writeIndent(out, indentLevel);
240+
out.write('$tag {\n');
241+
value.writeTextFormat(out, indentLevel + 1);
242+
writeIndent(out, indentLevel);
243+
out.write('}\n');
244+
}
245+
}
246+
183247
void writeToCodedBufferWriter(CodedBufferWriter output) {
184248
for (final entry in _fields.entries) {
185249
entry.value.writeTo(entry.key, output);

protobuf/lib/src/protobuf/utils.dart

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,57 @@ bool areMapsEqual(Map<Object?, Object?> lhs, Map<Object?, Object?> rhs) {
3232

3333
List<T> sorted<T>(Iterable<T> list) => List.from(list)..sort();
3434

35+
/// Escapes slash, double quotes, and newlines in [s] with \ as needed
36+
/// for a TextFormat string.
37+
///
38+
/// This is a copy of the official Java implementation: https://github.com/protocolbuffers/protobuf/blob/main/java/core/src/main/java/com/google/protobuf/TextFormat.java#L632
39+
String escapeString(String s) {
40+
return s
41+
.replaceAll('\\', '\\\\')
42+
.replaceAll('"', '\\"')
43+
.replaceAll('\n', '\\n');
44+
}
45+
46+
/// Appends the characters of [bytes] to [out] while escaping them as needed
47+
/// for a TextFormat string.
48+
///
49+
/// See TextFormat spec in https://protobuf.dev/reference/protobuf/textformat-spec/
50+
/// This is a copy of the official Java implementation: https://github.com/protocolbuffers/protobuf/blob/main/java/core/src/main/java/com/google/protobuf/TextFormatEscaper.java#L40
51+
void escapeBytes(List<int> bytes, StringSink out) {
52+
for (final byte in bytes) {
53+
// Only ASCII characters between 0x20 (space) and 0x7e (tilde) are
54+
// printable. Other byte values must be escaped.
55+
switch (byte) {
56+
case 0x07:
57+
out.write(r'\a');
58+
case 0x08:
59+
out.write(r'\b');
60+
case 0x0c:
61+
out.write(r'\f');
62+
case 0x0a:
63+
out.write(r'\n');
64+
case 0x0d:
65+
out.write(r'\r');
66+
case 0x09:
67+
out.write(r'\t');
68+
case 0x0b:
69+
out.write(r'\v');
70+
default:
71+
if (byte >= 0x20 && byte < 0x7f) {
72+
if (byte == 0x22 /* " */ || byte == 0x5c /* \ */ ) {
73+
out.write(r'\');
74+
}
75+
out.writeCharCode(byte);
76+
} else {
77+
out.write(r'\');
78+
out.write(((byte >> 6) & 3).toString());
79+
out.write(((byte >> 3) & 7).toString());
80+
out.write((byte & 7).toString());
81+
}
82+
}
83+
}
84+
}
85+
3586
class HashUtils {
3687
// Jenkins hash functions copied from
3788
// https://github.com/google/quiver-dart/blob/master/lib/src/core/hash.dart.

protobuf/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: protobuf
2-
version: 5.1.0
2+
version: 5.2.0
33
description: >-
44
Runtime library for protocol buffers support. Use with package:protoc_plugin
55
to generate Dart code for your '.proto' files.

protobuf/test/json_test.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import 'dart:convert';
1111
import 'package:fixnum/fixnum.dart' show Int64;
1212
import 'package:test/test.dart';
1313

14-
import 'mock_util.dart' show T, mockEnumValues;
14+
import 'mock_util.dart' show MockEnum, T;
1515

1616
void main() {
1717
test('mergeFromProto3Json unknown enum fields with names', () {
@@ -21,7 +21,7 @@ void main() {
2121
expect(example.hasEnm, isFalse);
2222

2323
// Defaults to first when it doesn't exist.
24-
expect(example.enm, equals(mockEnumValues.first));
24+
expect(example.enm, equals(MockEnum.values.first));
2525
expect((example..mergeFromProto3Json({'enm': 'a'})).enm.name, equals('a'));
2626

2727
// Now it's explicitly set after merging.

protobuf/test/map_mixin_test.dart

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,34 @@ void main() {
3232

3333
expect(r.isEmpty, false);
3434
expect(r.isNotEmpty, true);
35-
expect(r.keys, ['val', 'str', 'child', 'int32s', 'int64', 'enm']);
35+
expect(r.keys, [
36+
'val',
37+
'str',
38+
'child',
39+
'int32s',
40+
'int64',
41+
'enm',
42+
'stringMap',
43+
'bytes',
44+
]);
3645

3746
expect(r['val'], 42);
3847
expect(r['str'], '');
3948
expect(r['child'], isA<Rec>());
4049
expect(r['child'].toString(), 'Rec(42, "")');
4150
expect(r['int32s'], []);
51+
expect(r['stringMap'], {});
52+
expect(r['bytes'], []);
4253

4354
final v = r.values;
44-
expect(v.length, 6);
55+
expect(v.length, 8);
4556
expect(v.first, 42);
4657
expect(v.toList()[1], '');
4758
expect(v.toList()[3].toString(), '[]');
4859
expect(v.toList()[4], 0);
4960
expect((v.toList()[5] as ProtobufEnum).name, 'a');
61+
expect(v.toList()[6], {});
62+
expect(v.toList()[7], []);
5063
});
5164

5265
test('operator []= sets record fields', () {

0 commit comments

Comments
 (0)