@@ -281,6 +281,12 @@ public final void writeFieldName(String name) throws IOException {
281281 if (!_inObject ) {
282282 _reportError ("Can not write field name: current context not Object but " +_pbContext .typeDesc ());
283283 }
284+ // [dataformats-binary#712] Within a `map`, each "field name" is a map key
285+ // that opens a new entry sub-message
286+ if (_pbContext .inMap ()) {
287+ _startMapEntry (name );
288+ return ;
289+ }
284290 ProtobufField f = _currField ;
285291 // important: use current field only if NOT repeated field; repeated
286292 // field means an array until START_OBJECT
@@ -312,8 +318,14 @@ public final void writeFieldName(SerializableString sstr) throws IOException {
312318 if (!_inObject ) {
313319 _reportError ("Can not write field name: current context not Object but " +_pbContext .typeDesc ());
314320 }
315- ProtobufField f = _currField ;
316321 final String name = sstr .getValue ();
322+ // [dataformats-binary#712] Within a `map`, each "field name" is a map key
323+ // that opens a new entry sub-message
324+ if (_pbContext .inMap ()) {
325+ _startMapEntry (name );
326+ return ;
327+ }
328+ ProtobufField f = _currField ;
317329 // important: use current field only if NOT repeated field; repeated
318330 // field means an array until START_OBJECT
319331 // NOTE: not ideal -- depends on if it really is sibling field of an array,
@@ -417,6 +429,11 @@ public final void writeStartArray() throws IOException
417429 _reportError ("Can not write START_ARRAY without field (message type " +_currMessage .getName ()+")" );
418430 return ; // never gets here but code analyzers can't see that
419431 }
432+ // [dataformats-binary#712] A `map` field is also "repeated" underneath, but
433+ // must be written as an Object, not an Array
434+ if (_currField .isMap ) {
435+ _reportError ("Can not write START_ARRAY: field '" +_currField .name +"' is a `map`; write START_OBJECT instead" );
436+ }
420437 if (!_currField .isArray ()) {
421438 _reportError ("Can not write START_ARRAY: field '" +_currField .name +"' not declared as 'repeated'" );
422439 }
@@ -470,6 +487,17 @@ public final void writeStartObject() throws IOException
470487 }
471488 _currMessage = _schema .getRootType ();
472489 // note: no buffering on root
490+ } else if (_currField .isMap ) {
491+ // [dataformats-binary#712] a `map<K,V>` field: the Object being opened is
492+ // a sequence of entry sub-messages, not a single one. Push a dedicated map
493+ // context; per-entry buffering happens in writeFieldName. No buffering of
494+ // the map as a whole.
495+ _pbContext = _pbContext .createChildMapContext (_currField );
496+ streamWriteConstraints ().validateNestingDepth (_pbContext .getNestingDepth ());
497+ _currMessage = _currField .getMessageType (); // the synthetic entry message
498+ _inObject = true ;
499+ _writeTag = true ;
500+ return ;
473501 } else {
474502 // but also, field value must be Message if so
475503 if (!_currField .isObject ) {
@@ -504,6 +532,28 @@ public final void writeEndObject() throws IOException
504532 if (!_inObject ) {
505533 _reportError ("Current context not Object but " +_pbContext .typeDesc ());
506534 }
535+ // [dataformats-binary#712] Closing a `map`: finalize the last open entry (if
536+ // any), then pop -- but do NOT finish-buffer the map as a whole (each entry
537+ // was already length-prefixed on its own).
538+ if (_pbContext .inMap ()) {
539+ if (_pbContext .isEntryOpen ()) {
540+ _pbContext .setEntryOpen (false );
541+ _finishBuffering ();
542+ }
543+ _pbContext = _pbContext .getParent ();
544+ if (_pbContext .inRoot ()) {
545+ if (!_complete ) {
546+ _complete ();
547+ }
548+ } else {
549+ _currMessage = _pbContext .getMessageType ();
550+ }
551+ _currField = _pbContext .getField ();
552+ boolean inObj = _pbContext .inObject ();
553+ _inObject = inObj ;
554+ _writeTag = inObj || !_pbContext .inArray () || !_currField .packed ;
555+ return ;
556+ }
507557 _pbContext = _pbContext .getParent ();
508558 if (_pbContext .inRoot ()) {
509559 if (!_complete ) {
@@ -1737,6 +1787,141 @@ private final int _writeTag(int ptr)
17371787 return ptr ;
17381788 }
17391789
1790+ /*
1791+ /**********************************************************
1792+ /* Internal map (`map<K,V>`) writes [dataformats-binary#712]
1793+ /**********************************************************
1794+ */
1795+
1796+ /**
1797+ * Opens a new {@code map} entry sub-message for the given key, finalizing the
1798+ * previous entry first if one is still open. Writes the entry's key (tag 1) and
1799+ * leaves {@link #_currField} pointing at the value field (tag 2) so the value
1800+ * write that follows targets it.
1801+ */
1802+ private void _startMapEntry (String name ) throws IOException
1803+ {
1804+ final ProtobufField mapField = _pbContext .getField ();
1805+ // Close the previous entry, if any (its length prefix is finalized here)
1806+ if (_pbContext .isEntryOpen ()) {
1807+ _pbContext .setEntryOpen (false );
1808+ _finishBuffering ();
1809+ }
1810+ // Each entry is a length-delimited sub-message, tagged with the map field's tag
1811+ _startBuffering (mapField .typedTag );
1812+ _pbContext .setEntryOpen (true );
1813+ _currMessage = mapField .getMessageType ();
1814+ // Write key (tag 1) ...
1815+ _writeTag = true ;
1816+ _writeMapKey (mapField .getKeyField (), name );
1817+ // ... then prime the value field (tag 2) for the value write that follows
1818+ _currField = mapField .getValueField ();
1819+ _writeTag = true ;
1820+ }
1821+
1822+ /**
1823+ * Writes a map key (always arriving as a {@code String}) using the key field's
1824+ * declared protobuf type. Key types are restricted (and validated during schema
1825+ * resolution) to integral / {@code bool} / {@code string}.
1826+ */
1827+ private void _writeMapKey (ProtobufField keyField , String name ) throws IOException
1828+ {
1829+ _currField = keyField ;
1830+ switch (keyField .type ) {
1831+ case STRING :
1832+ writeString (name );
1833+ break ;
1834+ case BOOLEAN :
1835+ writeBoolean (_parseBooleanMapKey (name , keyField ));
1836+ break ;
1837+ case VINT32_Z : // `sint32`: signed only
1838+ writeNumber (_parseIntMapKey (name , keyField , false ));
1839+ break ;
1840+ case VINT32_STD : // `int32` or `uint32`
1841+ case FIXINT32 : // `sfixed32` or `fixed32`
1842+ writeNumber (_parseIntMapKey (name , keyField , true ));
1843+ break ;
1844+ case VINT64_Z : // `sint64`: signed only
1845+ writeNumber (_parseLongMapKey (name , keyField , false ));
1846+ break ;
1847+ case VINT64_STD : // `int64` or `uint64`
1848+ case FIXINT64 : // `sfixed64` or `fixed64`
1849+ writeNumber (_parseLongMapKey (name , keyField , true ));
1850+ break ;
1851+ default :
1852+ // Should not happen: key types are validated during schema resolution
1853+ _reportError ("Unsupported `map` key type " +keyField .type +" for field '" +keyField .name +"'" );
1854+ }
1855+ }
1856+
1857+ /**
1858+ * Parses a 32-bit {@code map} key.
1859+ *<p>
1860+ * {@link FieldType} does not distinguish signed from unsigned declarations
1861+ * ({@code int32} and {@code uint32} are both {@code VINT32_STD}), so for those the
1862+ * accepted range is the union of the two: {@code [Integer.MIN_VALUE, 0xFFFFFFFF]}.
1863+ * Narrowing a value above {@code Integer.MAX_VALUE} keeps the same 32 bits, which is
1864+ * exactly the {@code uint32} / {@code fixed32} encoding.
1865+ *
1866+ * @param allowUnsigned Whether the key type has an unsigned variant; {@code false}
1867+ * for {@code sint32}, which is always signed.
1868+ */
1869+ private int _parseIntMapKey (String name , ProtobufField keyField , boolean allowUnsigned )
1870+ throws IOException
1871+ {
1872+ final long l ;
1873+ try {
1874+ l = Long .parseLong (name );
1875+ } catch (NumberFormatException e ) {
1876+ _reportError ("Invalid `map` key \" " +name +"\" for integral key field '" +keyField .name
1877+ +"' (type " +keyField .type +")" );
1878+ return 0 ; // never reached
1879+ }
1880+ final long max = allowUnsigned ? 0xFFFFFFFFL : Integer .MAX_VALUE ;
1881+ if ((l < Integer .MIN_VALUE ) || (l > max )) {
1882+ _reportError ("Invalid `map` key \" " +name +"\" for integral key field '" +keyField .name
1883+ +"' (type " +keyField .type +"): out of range (" +Integer .MIN_VALUE +" to " +max +")" );
1884+ }
1885+ return (int ) l ;
1886+ }
1887+
1888+ /**
1889+ * Parses a 64-bit {@code map} key. As with 32-bit keys the signed and unsigned
1890+ * declarations share a {@link FieldType}, so {@code uint64} keys above
1891+ * {@link Long#MAX_VALUE} are accepted as unsigned: the resulting bit pattern is the
1892+ * same 64-bit varint either way.
1893+ *
1894+ * @param allowUnsigned Whether the key type has an unsigned variant; {@code false}
1895+ * for {@code sint64}, which is always signed.
1896+ */
1897+ private long _parseLongMapKey (String name , ProtobufField keyField , boolean allowUnsigned )
1898+ throws IOException
1899+ {
1900+ try {
1901+ return Long .parseLong (name );
1902+ } catch (NumberFormatException e ) {
1903+ if (allowUnsigned ) {
1904+ try {
1905+ return Long .parseUnsignedLong (name );
1906+ } catch (NumberFormatException e2 ) { }
1907+ }
1908+ _reportError ("Invalid `map` key \" " +name +"\" for integral key field '" +keyField .name
1909+ +"' (type " +keyField .type +")" );
1910+ return 0L ; // never reached
1911+ }
1912+ }
1913+
1914+ private boolean _parseBooleanMapKey (String name , ProtobufField keyField ) throws IOException {
1915+ if ("true" .equals (name )) {
1916+ return true ;
1917+ }
1918+ if ("false" .equals (name )) {
1919+ return false ;
1920+ }
1921+ _reportError ("Invalid `map` key \" " +name +"\" for boolean key field '" +keyField .name +"'" );
1922+ return false ; // never reached
1923+ }
1924+
17401925 /**
17411926 * Method called when buffering an entry that should be prefixed
17421927 * with a type tag.
0 commit comments