This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new a3154ced05 Add binary-native opt-in token surface for CBOR and MsgPack.
a3154ced05 is described below
commit a3154ced05f944cbfc6103f4d812414dd4df14dc
Author: James Bognar <[email protected]>
AuthorDate: Tue Jun 16 08:13:13 2026 -0400
Add binary-native opt-in token surface for CBOR and MsgPack.
Exposes the binary-native value types of CBOR (semantic tags + simple
values) and
MsgPack (`ext`) as opt-in token-level metadata, while the default
parseTokens /
serializeTokens cursors stay byte-for-byte unchanged.
- New `marshall.stream` role interfaces + `BinaryNativeKind` discriminant
(NONE / CBOR_TAG / CBOR_SIMPLE / MSGPACK_EXT);
`BinaryNativeTokenReader`/`Writer`
with per-format flavor markers and narrowing cursor return types.
- Explicit `parseNativeTokens(...)` / `serializeNativeTokens(...)`
factories (not a flag
on the default factories); native info surfaced via metadata accessors,
no new TokenType.
- Low-level `CborOutputStream.writeSimple` / `MsgPackOutputStream.writeExt`
primitives plus
flag-gated reader/writer support in both formats.
- Tests: new CBOR/MsgPack native-token-stream suites + invariant +
generic-consumer tests;
`@SuppressWarnings("resource")` on the fluent output-stream test helpers
(the discarded
fluent return is the try-with-resources-managed stream, not a leak).
Co-authored-by: Cursor <[email protected]>
---
.../juneau/marshall/cbor/CborOutputStream.java | 16 ++
.../apache/juneau/marshall/cbor/CborParser.java | 66 +++++
.../juneau/marshall/cbor/CborParserSession.java | 8 +-
.../juneau/marshall/cbor/CborTokenReader.java | 79 +++++-
.../juneau/marshall/cbor/CborTokenWriter.java | 19 ++
.../marshall/msgpack/MsgPackOutputStream.java | 29 +++
.../juneau/marshall/msgpack/MsgPackParser.java | 66 ++++-
.../marshall/msgpack/MsgPackParserSession.java | 8 +-
.../marshall/msgpack/MsgPackTokenReader.java | 61 ++++-
.../marshall/msgpack/MsgPackTokenWriter.java | 11 +
.../juneau/marshall/stream/BinaryNativeKind.java | 63 +++++
.../apache/juneau/marshall/stream/TokenReader.java | 62 +++++
.../apache/juneau/marshall/stream/TokenWriter.java | 55 +++++
.../marshall/cbor/CborNativeTokenStream_Test.java | 267 +++++++++++++++++++++
.../marshall/cbor/CborOutputStream_Test.java | 19 ++
.../msgpack/MsgPackNativeTokenStream_Test.java | 193 +++++++++++++++
.../marshall/msgpack/MsgPackOutputStream_Test.java | 93 +++++++
.../BinaryNativeKind_GenericConsumer_Test.java | 119 +++++++++
.../stream/FormatStreamingCapability_Test.java | 1 +
19 files changed, 1230 insertions(+), 5 deletions(-)
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborOutputStream.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborOutputStream.java
index 2b747ebdbd..dffe7f686e 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborOutputStream.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborOutputStream.java
@@ -398,4 +398,20 @@ public class CborOutputStream extends OutputStream {
writeHead(6, tagNumber);
return this;
}
+
+ /**
+ * Writes a CBOR simple value (major type 7).
+ *
+ * <p>
+ * Caller responsibility: reserved encodings (20-23 =
bool/null/undefined; 25-27 =
+ * float16/32/64; 31 = break) collide with native scalar/structural
emits and must not be used
+ * here.
+ *
+ * @param value The simple value (range {@code 0..255}; reserved values
noted above).
+ * @return This stream.
+ */
+ CborOutputStream writeSimple(int value) {
+ writeHead(7, value);
+ return this;
+ }
}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParser.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParser.java
index 128b74f5cc..607e362ed8 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParser.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParser.java
@@ -17,6 +17,7 @@
package org.apache.juneau.marshall.cbor;
import static org.apache.juneau.commons.utils.AssertionUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
import java.io.*;
import java.math.*;
@@ -78,6 +79,9 @@ public class CborParser extends InputStreamParser implements
CborMetaProvider, T
// Argument name constants for assertArgNotNull
private static final String ARG_copyFrom = "copyFrom";
+ // Property name constants
+ private static final String PROP_nativeMode = "nativeMode";
+
/** Default parser, string input encoded as BASE64. */
public static class Base64 extends CborParser {
@@ -98,11 +102,14 @@ public class CborParser extends InputStreamParser
implements CborMetaProvider, T
private static final Cache<HashKey,CborParser> CACHE =
Cache.of(HashKey.class, CborParser.class).build();
+ private boolean nativeMode;
+
/**
* Constructor, default settings.
*/
protected Builder() {
consumes("application/cbor");
+ nativeMode = env("CborParser.nativeMode", false);
}
/**
@@ -112,6 +119,7 @@ public class CborParser extends InputStreamParser
implements CborMetaProvider, T
*/
protected Builder(Builder copyFrom) {
super(assertArgNotNull(ARG_copyFrom, copyFrom));
+ nativeMode = copyFrom.nativeMode;
}
/**
@@ -121,6 +129,39 @@ public class CborParser extends InputStreamParser
implements CborMetaProvider, T
*/
protected Builder(CborParser copyFrom) {
super(assertArgNotNull(ARG_copyFrom, copyFrom));
+ nativeMode = copyFrom.nativeMode;
+ }
+
+ /**
+ * Surfaces CBOR semantic tags and simple values as token-level
metadata on
+ * {@link TokenReader} cursors returned by {@link
CborParserSession#parseTokens(Object)}.
+ *
+ * <p>
+ * When enabled, tags accumulate on the cursor's tag stack
(visible via
+ * {@link TokenReader#getTagCount()} / {@link
TokenReader#getTag(int)}) and simple values
+ * surface their int via {@link TokenReader#getSimpleValue()} on
+ * {@link TokenType#VALUE_NULL} tokens. When disabled
(default), tags are silently
+ * unwrapped and simple values collapse to {@link
TokenType#VALUE_NULL} with no metadata.
+ *
+ * <p>
+ * This is a token-cursor-level setting and does not affect the
high-level POJO databind
+ * path ({@link CborParser#parse(Object, Class)}); that path
always discards tags.
+ *
+ * @return This object.
+ */
+ public Builder nativeMode() {
+ return nativeMode(true);
+ }
+
+ /**
+ * Same as {@link #nativeMode()} but allows you to explicitly
specify the value.
+ *
+ * @param value The value for this setting.
+ * @return This object.
+ */
+ public Builder nativeMode(boolean value) {
+ nativeMode = value;
+ return this;
}
@Override /* Overridden from Context.Builder<?> */
@@ -132,6 +173,11 @@ public class CborParser extends InputStreamParser
implements CborMetaProvider, T
public Builder copy() {
return new Builder(this);
}
+
+ @Override /* Overridden from Context.Builder<?> */
+ public HashKey hashKey() {
+ return HashKey.of(super.hashKey(), nativeMode);
+ }
}
/** Default parser, string input encoded as spaced-hex. */
@@ -149,6 +195,8 @@ public class CborParser extends InputStreamParser
implements CborMetaProvider, T
/** Default parser, all default settings. */
public static final CborParser DEFAULT = new CborParser(create());
+ /** Default parser with binary-native opt-in mode enabled (CBOR tags /
simple values surface as token-level metadata). */
+ public static final CborParser DEFAULT_NATIVE = new
CborParser(create().nativeMode());
/** Default parser, string input encoded as spaced-hex. */
public static final CborParser DEFAULT_SPACED_HEX = new
SpacedHex(create());
/** Default parser, string input encoded as BASE64. */
@@ -163,6 +211,8 @@ public class CborParser extends InputStreamParser
implements CborMetaProvider, T
return new Builder();
}
+ protected final boolean nativeMode;
+
private final Map<ClassMeta<?>,CborClassMeta> cborClassMetas = new
ConcurrentHashMap<>();
private final Map<BeanPropertyMeta,CborBeanPropertyMeta>
cborBeanPropertyMetas = new ConcurrentHashMap<>();
@@ -173,6 +223,16 @@ public class CborParser extends InputStreamParser
implements CborMetaProvider, T
*/
public CborParser(Builder builder) {
super(builder);
+ this.nativeMode = builder.nativeMode;
+ }
+
+ /**
+ * Returns <jk>true</jk> if token-cursor native mode is enabled.
+ *
+ * @return <jk>true</jk> if native mode is enabled, else <jk>false</jk>.
+ */
+ public boolean isNativeMode() {
+ return nativeMode;
}
@Override /* Overridden from Context */
@@ -234,4 +294,10 @@ public class CborParser extends InputStreamParser
implements CborMetaProvider, T
public RecordReader parseArrayRecords(Object input) throws IOException {
return getSession().parseArrayRecords(input);
}
+
+ @Override /* Overridden from InputStreamParser */
+ protected FluentMap<String,Object> properties() {
+ return super.properties()
+ .a(PROP_nativeMode, nativeMode);
+ }
}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
index cfa6abb944..5631681fdc 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
@@ -57,6 +57,8 @@ public class CborParserSession extends
InputStreamParserSession implements Token
*/
public static class Builder extends
InputStreamParserSession.Builder<Builder> {
+ final boolean nativeMode;
+
/**
* Constructor
*
@@ -65,6 +67,7 @@ public class CborParserSession extends
InputStreamParserSession implements Token
*/
protected Builder(CborParser ctx) {
super(assertArgNotNull(ARG_ctx, ctx));
+ this.nativeMode = ctx.isNativeMode();
}
@Override
@@ -85,6 +88,8 @@ public class CborParserSession extends
InputStreamParserSession implements Token
return new Builder(assertArgNotNull(ARG_ctx, ctx));
}
+ private final boolean nativeMode;
+
/**
* Constructor.
*
@@ -92,6 +97,7 @@ public class CborParserSession extends
InputStreamParserSession implements Token
*/
protected CborParserSession(Builder builder) {
super(builder);
+ this.nativeMode = builder.nativeMode;
}
/**
@@ -119,7 +125,7 @@ public class CborParserSession extends
InputStreamParserSession implements Token
@Override /* TokenReadable */
public TokenReader parseTokens(Object input) throws IOException {
var pipe = new ParserPipe(input, isDebug(),
isAutoCloseStreams(), isUnbuffered(), null);
- return new CborTokenReader(pipe, this);
+ return new CborTokenReader(pipe,
this).setNativeMode(nativeMode);
}
/**
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborTokenReader.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborTokenReader.java
index 6ebc3d02cd..15b33bcdf0 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborTokenReader.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborTokenReader.java
@@ -86,6 +86,17 @@ public class CborTokenReader implements TokenReader {
private boolean currentBoolean;
private byte[] currentBinary;
+ // Binary-native opt-in state (175ad). Defaults preserve the
normalize-by-default contract:
+ // nativeMode == false ⇒ tag numbers are dropped, simple values surface
as VALUE_NULL with no
+ // metadata. When nativeMode is true, the TAG branch accumulates tag
numbers into 'tags' as
+ // it recurses, the SIMPLE branch captures the simple int into
'simpleValue', and 'nativeKind'
+ // classifies the resulting token for format-agnostic consumers.
+ private boolean nativeMode;
+ private BinaryNativeKind nativeKind = BinaryNativeKind.NONE;
+ private long[] tags = new long[4];
+ private int tagCount;
+ private int simpleValue;
+
/**
* Constructor with default settings.
*
@@ -128,6 +139,24 @@ public class CborTokenReader implements TokenReader {
return this;
}
+ /**
+ * Enables or disables binary-native opt-in mode.
+ *
+ * <p>
+ * When enabled, CBOR semantic tags are captured on the tag stack
rather than discarded
+ * (surface them via {@link #getTagCount()} / {@link #getTag(int)}),
and CBOR simple values
+ * surface their int via {@link #getSimpleValue()}. When disabled
(default), tags are
+ * silently unwrapped and simple values collapse to {@link
TokenType#VALUE_NULL} with no
+ * metadata — identical to the pre-native-mode behavior.
+ *
+ * @param value <jk>true</jk> to enable native mode.
+ * @return This object.
+ */
+ public CborTokenReader setNativeMode(boolean value) {
+ nativeMode = value;
+ return this;
+ }
+
//
==============================================================================================
// State-machine summary. CBOR is already a token-shaped wire format
(RFC 8949 major types), so
// next() reads the next data-type tag via
CborInputStream.readDataType() and dispatches based
@@ -136,7 +165,7 @@ public class CborTokenReader implements TokenReader {
// END_*), and (3) within a map, whether we're at a key position or
value position.
//
// Per-level container state lives on parallel stacks (stackRemaining +
stackIsMap +
- // stackAwaitingKey, indexed by depth). A stackRemaining value of -1
means indefinite-length;
+ // stackAwaitingKey, indexed by depth). A stackRemaining value of -1
means indefinite-length
// in that case the next() loop relies on CborInputStream returning
DataType.BREAK instead of
// counting elements.
//
==============================================================================================
@@ -154,6 +183,12 @@ public class CborTokenReader implements TokenReader {
currentNumberLexeme = null;
currentNumber = null;
currentBinary = null;
+ // Native-mode metadata: only reset at outermost entry —
recursive tag unwraps preserve the
+ // accumulated tag stack so nested tag(tag(value)) round-trips
losslessly.
+ if (tagNestingDepth == 0) {
+ nativeKind = BinaryNativeKind.NONE;
+ tagCount = 0;
+ }
// Inside a definite-length container at element-count zero:
emit END_*.
if (depth > 0 && stackRemaining[depth - 1] == 0) {
@@ -241,7 +276,16 @@ public class CborTokenReader implements TokenReader {
case TAG, SIMPLE -> {
// Q3: by default normalize to common scalars.
TAG: skip the tag and read the
// next item as the wrapped value. SIMPLE:
emit as VALUE_NULL (best-effort).
+ // In native mode (175ad): TAG numbers
accumulate on the tag stack across
+ // recursion; SIMPLE captures the simple int as
metadata on the VALUE_NULL token.
if (dt == DataType.TAG) {
+ var n = is.readLength();
+ if (nativeMode) {
+ if (tagCount == tags.length)
+ tags =
java.util.Arrays.copyOf(tags, tags.length * 2);
+ tags[tagCount++] = n;
+ nativeKind =
BinaryNativeKind.CBOR_TAG;
+ }
// Guard against pathologically deep
tag chains blowing the JVM stack.
if (tagNestingDepth >=
maxTagNestingDepth)
throw new ParseException(
@@ -254,6 +298,10 @@ public class CborTokenReader implements TokenReader {
tagNestingDepth--;
}
}
+ if (nativeMode) {
+ simpleValue = (int) is.readLength();
+ nativeKind =
BinaryNativeKind.CBOR_SIMPLE;
+ }
currentToken = TokenType.VALUE_NULL;
consumedOneElement();
}
@@ -446,4 +494,33 @@ public class CborTokenReader implements TokenReader {
public void close() throws IOException {
pipe.close();
}
+
+ @Override /* TokenReader */
+ public BinaryNativeKind getNativeKind() {
+ return nativeKind;
+ }
+
+ @Override /* TokenReader */
+ public int getTagCount() {
+ return tagCount;
+ }
+
+ @Override /* TokenReader */
+ public long getTag(int index) {
+ if (index < 0 || index >= tagCount)
+ throw new IndexOutOfBoundsException("Tag index " +
index + " out of bounds for tagCount " + tagCount);
+ return tags[index];
+ }
+
+ @Override /* TokenReader */
+ public int getExtType() {
+ throw new IllegalStateException("CBOR cursor has no MsgPack ext
type.");
+ }
+
+ @Override /* TokenReader */
+ public int getSimpleValue() {
+ if (nativeKind != BinaryNativeKind.CBOR_SIMPLE)
+ throw new IllegalStateException("Current token is not a
CBOR simple value (nativeKind=" + nativeKind + ")");
+ return simpleValue;
+ }
}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborTokenWriter.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborTokenWriter.java
index 99fb06ada8..d1a4a46045 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborTokenWriter.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborTokenWriter.java
@@ -265,6 +265,25 @@ public class CborTokenWriter implements TokenWriter {
return this;
}
+ @Override /* TokenWriter */
+ public CborTokenWriter writeTag(long tagNumber) throws IOException {
+ assertOpen();
+ // A tag is a prefix on the next value emit; it does NOT
consume a map-key/value or
+ // array-element slot. Skip preValueWrite()/afterValue() — the
wrapped value emit that
+ // follows owns the state transition.
+ out.writeTag(tagNumber);
+ return this;
+ }
+
+ @Override /* TokenWriter */
+ public CborTokenWriter writeSimple(int value) throws IOException {
+ assertOpen();
+ preValueWrite();
+ out.writeSimple(value);
+ afterValue();
+ return this;
+ }
+
@Override /* TokenWriter */
public TokenWriter object(Object value) throws IOException {
assertOpen();
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackOutputStream.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackOutputStream.java
index 5723c59e7b..db30edda13 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackOutputStream.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackOutputStream.java
@@ -177,6 +177,35 @@ public class MsgPackOutputStream extends OutputStream {
return append1((int)(l >> 56)).append1((int)(l >>
48)).append1((int)(l >> 40)).append1((int)(l >> 32)).append1((int)(l >>
24)).append1((int)(l >> 16)).append1((int)(l >> 8)).append1((int)(l));
}
+ /**
+ * Appends a MsgPack {@code ext} field (typed binary) to the stream.
+ *
+ * <p>
+ * Selects the smallest framing that fits the payload length:
fixext1/2/4/8/16 for the five
+ * exact lengths, else ext8 / ext16 / ext32 with a length prefix; the
type byte and payload
+ * are appended after the framing header.
+ *
+ * @param type The {@code ext} type byte (signed; range {@code
-128..127}).
+ * @param payload The {@code ext} payload bytes.
+ * @return This stream.
+ */
+ MsgPackOutputStream writeExt(int type, byte[] payload) {
+ var n = payload.length;
+ switch (n) {
+ case 1: return
append1(FIXEXT1).append1(type).append(payload);
+ case 2: return
append1(FIXEXT2).append1(type).append(payload);
+ case 4: return
append1(FIXEXT4).append1(type).append(payload);
+ case 8: return
append1(FIXEXT8).append1(type).append(payload);
+ case 16: return
append1(FIXEXT16).append1(type).append(payload);
+ default: break;
+ }
+ if (n < (1 << 8))
+ return
append1(EXT8).append1(n).append1(type).append(payload);
+ if (n < (1 << 16))
+ return
append1(EXT16).append2(n).append1(type).append(payload);
+ return append1(EXT32).append4(n).append1(type).append(payload);
+ }
+
/**
* Appends a binary field to the stream.
*/
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParser.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParser.java
index e0435cb244..44871948f0 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParser.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParser.java
@@ -17,6 +17,7 @@
package org.apache.juneau.marshall.msgpack;
import static org.apache.juneau.commons.utils.AssertionUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
import java.io.*;
import java.util.*;
@@ -73,6 +74,9 @@ public class MsgPackParser extends InputStreamParser
implements MsgPackMetaProvi
// Argument name constants for assertArgNotNull
private static final String ARG_copyFrom = "copyFrom";
+ // Property name constants
+ private static final String PROP_nativeMode = "nativeMode";
+
/** Default parser, string input encoded as BASE64. */
public static class Base64 extends MsgPackParser {
@@ -93,11 +97,14 @@ public class MsgPackParser extends InputStreamParser
implements MsgPackMetaProvi
private static final Cache<HashKey,MsgPackParser> CACHE =
Cache.of(HashKey.class, MsgPackParser.class).build();
+ private boolean nativeMode;
+
/**
* Constructor, default settings.
*/
protected Builder() {
consumes("application/msgpack,octal/msgpack");
+ nativeMode = env("MsgPackParser.nativeMode", false);
}
/**
@@ -108,6 +115,7 @@ public class MsgPackParser extends InputStreamParser
implements MsgPackMetaProvi
*/
protected Builder(Builder copyFrom) {
super(assertArgNotNull(ARG_copyFrom, copyFrom));
+ nativeMode = copyFrom.nativeMode;
}
/**
@@ -118,6 +126,39 @@ public class MsgPackParser extends InputStreamParser
implements MsgPackMetaProvi
*/
protected Builder(MsgPackParser copyFrom) {
super(assertArgNotNull(ARG_copyFrom, copyFrom));
+ nativeMode = copyFrom.nativeMode;
+ }
+
+ /**
+ * Surfaces the MsgPack {@code ext} type byte as token-level
metadata on
+ * {@link TokenReader} cursors returned by {@link
MsgPackParserSession#parseTokens(Object)}.
+ *
+ * <p>
+ * When enabled, ext tokens carry the signed type byte (visible
via
+ * {@link TokenReader#getExtType()}) on {@link
TokenType#VALUE_BINARY} tokens. When
+ * disabled (default), the payload still surfaces as {@link
TokenType#VALUE_BINARY} but
+ * the type byte is dropped.
+ *
+ * <p>
+ * This is a token-cursor-level setting and does not affect the
high-level POJO databind
+ * path ({@link MsgPackParser#parse(Object, Class)}); that path
always discards the type
+ * byte.
+ *
+ * @return This object.
+ */
+ public Builder nativeMode() {
+ return nativeMode(true);
+ }
+
+ /**
+ * Same as {@link #nativeMode()} but allows you to explicitly
specify the value.
+ *
+ * @param value The value for this setting.
+ * @return This object.
+ */
+ public Builder nativeMode(boolean value) {
+ nativeMode = value;
+ return this;
}
@Override /* Overridden from Context.Builder<?> */
@@ -130,7 +171,10 @@ public class MsgPackParser extends InputStreamParser
implements MsgPackMetaProvi
return new Builder(this);
}
-
+ @Override /* Overridden from Context.Builder<?> */
+ public HashKey hashKey() {
+ return HashKey.of(super.hashKey(), nativeMode);
+ }
}
/** Default parser, string input encoded as spaced-hex. */
@@ -148,6 +192,8 @@ public class MsgPackParser extends InputStreamParser
implements MsgPackMetaProvi
/** Default parser, all default settings.*/
public static final MsgPackParser DEFAULT = new MsgPackParser(create());
+ /** Default parser with binary-native opt-in mode enabled (MsgPack
{@code ext} type byte surfaces as token-level metadata). */
+ public static final MsgPackParser DEFAULT_NATIVE = new
MsgPackParser(create().nativeMode());
/** Default parser, all default settings, string input encoded as
spaced-hex.*/
public static final MsgPackParser DEFAULT_SPACED_HEX = new
SpacedHex(create());
@@ -163,6 +209,8 @@ public class MsgPackParser extends InputStreamParser
implements MsgPackMetaProvi
return new Builder();
}
+ protected final boolean nativeMode;
+
private final Map<ClassMeta<?>,MsgPackClassMeta> msgPackClassMetas =
new ConcurrentHashMap<>();
private final Map<BeanPropertyMeta,MsgPackBeanPropertyMeta>
msgPackBeanPropertyMetas = new ConcurrentHashMap<>();
@@ -173,6 +221,16 @@ public class MsgPackParser extends InputStreamParser
implements MsgPackMetaProvi
*/
public MsgPackParser(Builder builder) {
super(builder);
+ this.nativeMode = builder.nativeMode;
+ }
+
+ /**
+ * Returns <jk>true</jk> if token-cursor native mode is enabled.
+ *
+ * @return <jk>true</jk> if native mode is enabled, else <jk>false</jk>.
+ */
+ public boolean isNativeMode() {
+ return nativeMode;
}
@Override /* Overridden from Context */
@@ -232,4 +290,10 @@ public class MsgPackParser extends InputStreamParser
implements MsgPackMetaProvi
public RecordReader parseArrayRecords(Object input) throws IOException {
return getSession().parseArrayRecords(input);
}
+
+ @Override /* Overridden from InputStreamParser */
+ protected FluentMap<String,Object> properties() {
+ return super.properties()
+ .a(PROP_nativeMode, nativeMode);
+ }
}
\ No newline at end of file
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
index 881e778f2c..491b4766bb 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
@@ -58,6 +58,8 @@ public class MsgPackParserSession extends
InputStreamParserSession implements To
*/
public static class Builder extends
InputStreamParserSession.Builder<Builder> {
+ final boolean nativeMode;
+
/**
* Constructor
*
@@ -66,6 +68,7 @@ public class MsgPackParserSession extends
InputStreamParserSession implements To
*/
protected Builder(MsgPackParser ctx) {
super(assertArgNotNull(ARG_ctx, ctx));
+ this.nativeMode = ctx.isNativeMode();
}
@Override
@@ -86,6 +89,8 @@ public class MsgPackParserSession extends
InputStreamParserSession implements To
return new Builder(assertArgNotNull(ARG_ctx, ctx));
}
+ private final boolean nativeMode;
+
/**
* Constructor.
*
@@ -93,6 +98,7 @@ public class MsgPackParserSession extends
InputStreamParserSession implements To
*/
protected MsgPackParserSession(Builder builder) {
super(builder);
+ this.nativeMode = builder.nativeMode;
}
/**
@@ -114,7 +120,7 @@ public class MsgPackParserSession extends
InputStreamParserSession implements To
@Override /* TokenReadable */
public TokenReader parseTokens(Object input) throws IOException {
var pipe = new ParserPipe(input, isDebug(),
isAutoCloseStreams(), isUnbuffered(), null);
- return new MsgPackTokenReader(pipe, this);
+ return new MsgPackTokenReader(pipe,
this).setNativeMode(nativeMode);
}
/**
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackTokenReader.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackTokenReader.java
index 040d3e2535..ea78ae019b 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackTokenReader.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackTokenReader.java
@@ -61,6 +61,14 @@ public class MsgPackTokenReader implements TokenReader {
private boolean currentBoolean;
private byte[] currentBinary;
+ // Binary-native opt-in state (175ad). Defaults preserve the
normalize-by-default contract:
+ // EXT payloads surface as VALUE_BINARY with no metadata. When
nativeMode is true, the EXT
+ // branch additionally captures the signed type byte into 'extType' and
sets
+ // 'nativeKind' = MSGPACK_EXT.
+ private boolean nativeMode;
+ private BinaryNativeKind nativeKind = BinaryNativeKind.NONE;
+ private int extType;
+
/**
* Constructor with default settings.
*
@@ -86,6 +94,23 @@ public class MsgPackTokenReader implements TokenReader {
this.session = session;
}
+ /**
+ * Enables or disables binary-native opt-in mode.
+ *
+ * <p>
+ * When enabled, MsgPack {@code ext} tokens carry the signed type byte
via
+ * {@link #getExtType()}. When disabled (default), the payload still
surfaces as
+ * {@link TokenType#VALUE_BINARY} but the type byte is dropped —
identical to the
+ * pre-native-mode behavior.
+ *
+ * @param value <jk>true</jk> to enable native mode.
+ * @return This object.
+ */
+ public MsgPackTokenReader setNativeMode(boolean value) {
+ nativeMode = value;
+ return this;
+ }
+
//
==============================================================================================
// State-machine summary. MsgPack containers are length-prefixed (no
indefinite-length
// encoding), so next() reads the data-type tag via
MsgPackInputStream.readDataType() and
@@ -107,6 +132,7 @@ public class MsgPackTokenReader implements TokenReader {
currentNumberLexeme = null;
currentNumber = null;
currentBinary = null;
+ nativeKind = BinaryNativeKind.NONE;
// Inside a container at element-count zero: emit END_*.
if (depth > 0 && stackRemaining[depth - 1] == 0) {
@@ -192,7 +218,13 @@ public class MsgPackTokenReader implements TokenReader {
consumedOneElement();
}
case EXT -> {
- // Q3 default: normalize to common scalars;
skip the extension payload as binary.
+ // Q3 default: normalize to common scalars; the
extension payload surfaces as
+ // VALUE_BINARY. In native mode (175ad), the
signed ext type byte is also
+ // captured for retrieval via getExtType().
+ if (nativeMode) {
+ extType = (byte) is.getExtType();
+ nativeKind =
BinaryNativeKind.MSGPACK_EXT;
+ }
currentBinary = is.readBinary();
currentToken = TokenType.VALUE_BINARY;
consumedOneElement();
@@ -364,4 +396,31 @@ public class MsgPackTokenReader implements TokenReader {
@Override /* TokenReader */
public void close() throws IOException { pipe.close(); }
+
+ @Override /* TokenReader */
+ public BinaryNativeKind getNativeKind() {
+ return nativeKind;
+ }
+
+ @Override /* TokenReader */
+ public int getTagCount() {
+ return 0;
+ }
+
+ @Override /* TokenReader */
+ public long getTag(int index) {
+ throw new IndexOutOfBoundsException("Tag index " + index + "
out of bounds for tagCount 0");
+ }
+
+ @Override /* TokenReader */
+ public int getExtType() {
+ if (nativeKind != BinaryNativeKind.MSGPACK_EXT)
+ throw new IllegalStateException("Current token is not a
MsgPack ext (nativeKind=" + nativeKind + ")");
+ return extType;
+ }
+
+ @Override /* TokenReader */
+ public int getSimpleValue() {
+ throw new IllegalStateException("MsgPack cursor has no CBOR
simple value.");
+ }
}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackTokenWriter.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackTokenWriter.java
index bb15173a3a..44f50a23f8 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackTokenWriter.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackTokenWriter.java
@@ -287,6 +287,17 @@ public class MsgPackTokenWriter implements TokenWriter {
return this;
}
+ @Override /* TokenWriter */
+ public MsgPackTokenWriter writeExt(int type, byte[] payload) throws
IOException {
+ assertOpen();
+ if (payload == null)
+ throw new IllegalArgumentException("ext payload must
not be null");
+ preValueCheck();
+ new MsgPackOutputStream(activeOut()).writeExt(type, payload);
+ afterValue();
+ return this;
+ }
+
@Override /* TokenWriter */
public TokenWriter object(Object value) throws IOException {
assertOpen();
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/stream/BinaryNativeKind.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/stream/BinaryNativeKind.java
new file mode 100644
index 0000000000..15e179f325
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/stream/BinaryNativeKind.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.marshall.stream;
+
+/**
+ * Discriminant returned by {@link TokenReader#getNativeKind()} that
classifies the
+ * binary-native metadata (if any) carried by the cursor's current token.
+ *
+ * <p>
+ * A {@link TokenReader} surfaces format-native value types as <b>metadata on
the existing
+ * token vocabulary</b> rather than as new top-level events. A native wrapper
rides on whatever
+ * token the wrapped value emits ({@code VALUE_*} or {@code START_*}); this
enum lets a
+ * format-agnostic consumer branch on the kind of native metadata without
having to test the
+ * cursor's runtime type.
+ *
+ * <p>
+ * Each enum value names the concrete native concept it represents. New
values may be added in
+ * future minor releases as additional formats expose native opt-in metadata.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li>{@link TokenReader} — the cursor that exposes this
discriminant.
+ * <li class='link'><a class="doclink"
href="https://juneau.apache.org/docs/topics/SerializersAndParsers">Serializers
and Parsers</a>
+ * </ul>
+ */
+public enum BinaryNativeKind {
+
+ /** The current token carries no binary-native metadata. */
+ NONE,
+
+ /**
+ * The current token is wrapped by one or more semantic tags (CBOR
major type 6). Surfaced
+ * via {@link TokenReader#getTagCount()} / {@link
TokenReader#getTag(int)}.
+ */
+ CBOR_TAG,
+
+ /**
+ * The current token represents a CBOR simple value (major type 7).
Token type is
+ * {@link TokenType#VALUE_NULL}; the simple int is surfaced via
+ * {@link TokenReader#getSimpleValue()}.
+ */
+ CBOR_SIMPLE,
+
+ /**
+ * The current token represents a MsgPack {@code ext} value (signed
type byte + binary
+ * payload). Token type is {@link TokenType#VALUE_BINARY}; the type
byte is surfaced via
+ * {@link TokenReader#getExtType()}.
+ */
+ MSGPACK_EXT
+}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/stream/TokenReader.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/stream/TokenReader.java
index 1870b7b84d..9cb27d5ce4 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/stream/TokenReader.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/stream/TokenReader.java
@@ -301,4 +301,66 @@ public interface TokenReader extends RecordReader {
*/
@Override
void close() throws IOException;
+
+ //
=================================================================================
+ // Binary-native opt-in metadata accessors. Default cursors and
formats without
+ // native concepts inherit the safe defaults below: getNativeKind()
returns NONE,
+ // getTagCount() returns 0, and the value accessors throw.
Format-specific cursors
+ // override the relevant subset (CBOR overrides
getNativeKind/getTagCount/getTag/
+ // getSimpleValue; MsgPack overrides getNativeKind/getExtType). See
+ // {@link BinaryNativeKind} for the catalog of native concepts.
+ //
=================================================================================
+
+ /**
+ * Returns the binary-native classification of the current token.
+ *
+ * @return The native kind, or {@link BinaryNativeKind#NONE NONE} when
the current token
+ * carries no binary-native metadata. Never {@code null}.
+ */
+ default BinaryNativeKind getNativeKind() {
+ return BinaryNativeKind.NONE;
+ }
+
+ /**
+ * Returns the number of semantic tags wrapping the current token.
+ *
+ * @return The tag count (0 when {@link #getNativeKind()} is not
+ * {@link BinaryNativeKind#CBOR_TAG CBOR_TAG}).
+ */
+ default int getTagCount() {
+ return 0;
+ }
+
+ /**
+ * Returns the semantic tag at the specified position in the tag stack.
+ *
+ * @param index The tag position; outermost tag is {@code 0}.
+ * @return The tag number.
+ * @throws IndexOutOfBoundsException If {@code index} is outside {@code
[0, getTagCount())}.
+ */
+ default long getTag(int index) {
+ throw new IndexOutOfBoundsException("Tag index " + index + "
out of bounds for tagCount " + getTagCount());
+ }
+
+ /**
+ * Returns the typed-binary type byte for the current token.
+ *
+ * @return The signed type byte (range {@code -128..127}).
+ * @throws IllegalStateException If {@link #getNativeKind()} is not
+ * {@link BinaryNativeKind#MSGPACK_EXT MSGPACK_EXT}.
+ */
+ default int getExtType() {
+ throw new IllegalStateException("Current token is not a
typed-binary (ext) value (nativeKind=" + getNativeKind() + ")");
+ }
+
+ /**
+ * Returns the opaque simple value for the current token.
+ *
+ * @return The simple int.
+ * @throws IllegalStateException If {@link #getNativeKind()} is not
+ * {@link BinaryNativeKind#CBOR_SIMPLE CBOR_SIMPLE}.
+ */
+ default int getSimpleValue() {
+ throw new IllegalStateException("Current token is not a simple
value (nativeKind=" + getNativeKind() + ")");
+ }
}
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/stream/TokenWriter.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/stream/TokenWriter.java
index 5f842f206d..0b486912c8 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/stream/TokenWriter.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/stream/TokenWriter.java
@@ -288,4 +288,59 @@ public interface TokenWriter extends RecordWriter {
*/
@Override
void close() throws IOException;
+
+ //
=================================================================================
+ // Binary-native opt-in emits. These default to
UnsupportedOperationException so
+ // formats override only the subset they support (CBOR overrides
writeTag and
+ // writeSimple; MsgPack overrides writeExt; text formats inherit the
throwing
+ // defaults). See {@link BinaryNativeKind} for the catalog of native
concepts.
+ //
=================================================================================
+
+ /**
+ * Emits a semantic tag prefixing the next value emit.
+ *
+ * <p>
+ * Tags compose: {@code writeTag(1).writeTag(2).number(5)} writes
{@code tag(1)(tag(2)(5))}.
+ * The tag header is written immediately and does NOT consume a
map-key/value or array-element
+ * slot; that bookkeeping belongs to the wrapped value emit that
follows.
+ *
+ * @param tagNumber The unsigned tag number (carried as a {@code long}
for the full unsigned
+ * 64-bit range).
+ * @return This object.
+ * @throws IOException If a problem occurred writing to the underlying
stream.
+ * @throws UnsupportedOperationException If the format does not support
semantic tags.
+ */
+ default TokenWriter writeTag(long tagNumber) throws IOException {
+ throw new UnsupportedOperationException("writeTag is not
supported by this writer.");
+ }
+
+ /**
+ * Emits an opaque simple value. The corresponding read-side token is
+ * {@link TokenType#VALUE_NULL} with {@link
TokenReader#getSimpleValue()}.
+ *
+ * @param value The simple value. Format-specific reserved encodings
(e.g. CBOR major-7
+ * codepoints 20-23, 25-27, 31 collide with
bool/null/undefined/float/break) must not be
+ * used.
+ * @return This object.
+ * @throws IOException If a problem occurred writing to the underlying
stream.
+ * @throws UnsupportedOperationException If the format does not support
opaque simple values.
+ */
+ default TokenWriter writeSimple(int value) throws IOException {
+ throw new UnsupportedOperationException("writeSimple is not
supported by this writer.");
+ }
+
+ /**
+ * Emits a typed-binary value (signed type byte + binary payload). The
corresponding
+ * read-side token is {@link TokenType#VALUE_BINARY} with {@link
TokenReader#getExtType()}
+ * and {@link TokenReader#getBinary()}.
+ *
+ * @param type The signed type byte (range {@code -128..127}).
+ * @param payload The payload bytes. Must not be {@code null}.
+ * @return This object.
+ * @throws IOException If a problem occurred writing to the underlying
stream.
+ * @throws UnsupportedOperationException If the format does not support
typed-binary values.
+ */
+ default TokenWriter writeExt(int type, byte[] payload) throws
IOException {
+ throw new UnsupportedOperationException("writeExt is not
supported by this writer.");
+ }
}
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/cbor/CborNativeTokenStream_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/cbor/CborNativeTokenStream_Test.java
new file mode 100644
index 0000000000..c1d59d99ff
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/cbor/CborNativeTokenStream_Test.java
@@ -0,0 +1,267 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.marshall.cbor;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.marshall.stream.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Unit tests for the CBOR opt-in binary-native token surface (175ad).
Verifies that
+ * {@link CborParserSession#parseNativeTokens(Object) parseNativeTokens}
surfaces semantic tags
+ * and simple values as token-level metadata, and that the default
+ * {@link CborParserSession#parseTokens(Object) parseTokens} path still
normalizes them away.
+ */
+@SuppressWarnings({
+ "resource" // Token readers are closed via try-with-resources; JDT's
flow analysis over chained factory calls yields false-positive leak reports.
+})
+class CborNativeTokenStream_Test extends TestBase {
+
+ private static byte[] bytes(int... b) {
+ var r = new byte[b.length];
+ for (var i = 0; i < b.length; i++) r[i] = (byte) b[i];
+ return r;
+ }
+
+ //
=================================================================================
+ // A. Reader native-mode (parseNativeTokens)
+ //
=================================================================================
+
+ @Test void a01_singleTag() throws Exception {
+ // 0xC0 = tag(0); 0x61 = string len 1; 'a' = 0x61 — tag(0)
wraps "a".
+ var data = bytes(0xC0, 0x61, 0x61);
+ try (var r =
CborParser.DEFAULT_NATIVE.getSession().parseTokens(data)) {
+ assertEquals(TokenType.VALUE_STRING, r.next());
+ assertEquals(BinaryNativeKind.CBOR_TAG,
r.getNativeKind());
+ assertEquals(1, r.getTagCount());
+ assertEquals(0L, r.getTag(0));
+ assertEquals("a", r.getString());
+ assertEquals(TokenType.END_OF_STREAM, r.next());
+ }
+ }
+
+ @Test void a02_nestedTags() throws Exception {
+ // tag(1)(tag(2)(uint 5)). tag-1 = 0xC1, tag-2 = 0xC2, uint 5
= 0x05.
+ var data = bytes(0xC1, 0xC2, 0x05);
+ try (var r =
CborParser.DEFAULT_NATIVE.getSession().parseTokens(data)) {
+ assertEquals(TokenType.VALUE_NUMBER, r.next());
+ assertEquals(BinaryNativeKind.CBOR_TAG,
r.getNativeKind());
+ assertEquals(2, r.getTagCount());
+ assertEquals(1L, r.getTag(0)); // outermost first
+ assertEquals(2L, r.getTag(1));
+ assertEquals(5L, r.getNumber().longValue());
+ }
+ }
+
+ @Test void a03_taggedContainer() throws Exception {
+ // tag(4) wrapping an empty array: 0xC4 0x80
+ var data = bytes(0xC4, 0x80);
+ try (var r =
CborParser.DEFAULT_NATIVE.getSession().parseTokens(data)) {
+ assertEquals(TokenType.START_ARRAY, r.next());
+ assertEquals(BinaryNativeKind.CBOR_TAG,
r.getNativeKind());
+ assertEquals(1, r.getTagCount());
+ assertEquals(4L, r.getTag(0));
+ // Subsequent END_ARRAY is not tagged.
+ assertEquals(TokenType.END_ARRAY, r.next());
+ assertEquals(BinaryNativeKind.NONE, r.getNativeKind());
+ assertEquals(0, r.getTagCount());
+ }
+ }
+
+ @Test void a04_simpleValue() throws Exception {
+ // Major-7 simple value 16: 0xF0 (initial byte = 0xE0 | 16).
+ var data = bytes(0xF0);
+ try (var r =
CborParser.DEFAULT_NATIVE.getSession().parseTokens(data)) {
+ assertEquals(TokenType.VALUE_NULL, r.next());
+ assertEquals(BinaryNativeKind.CBOR_SIMPLE,
r.getNativeKind());
+ assertEquals(16, r.getSimpleValue());
+ }
+ }
+
+ @Test void a05_defaultStillNormalizes() throws Exception {
+ // Same inputs via plain parseTokens: tags are silently
unwrapped, simple → VALUE_NULL.
+ var taggedData = bytes(0xC1, 0xC2, 0x05);
+ try (var r = CborParser.DEFAULT.parseTokens(taggedData)) {
+ assertEquals(TokenType.VALUE_NUMBER, r.next());
+ assertEquals(5L, r.getNumber().longValue());
+ }
+ var simpleData = bytes(0xF0);
+ try (var r = CborParser.DEFAULT.parseTokens(simpleData)) {
+ assertEquals(TokenType.VALUE_NULL, r.next());
+ }
+ }
+
+ @Test void a06_extTypeAccessThrowsOnCbor() throws Exception {
+ var data = bytes(0xC0, 0x61, 0x61);
+ try (var r =
CborParser.DEFAULT_NATIVE.getSession().parseTokens(data)) {
+ assertEquals(TokenType.VALUE_STRING, r.next());
+ assertThrows(IllegalStateException.class,
r::getExtType);
+ }
+ }
+
+ @Test void a07_simpleValueOutOfStateThrows() throws Exception {
+ // A plain string token has no simple value: getSimpleValue
must throw.
+ var data = bytes(0x61, 0x61);
+ try (var r =
CborParser.DEFAULT_NATIVE.getSession().parseTokens(data)) {
+ assertEquals(TokenType.VALUE_STRING, r.next());
+ assertThrows(IllegalStateException.class,
r::getSimpleValue);
+ }
+ }
+
+ @Test void a08_getTagOutOfBounds() throws Exception {
+ var data = bytes(0xC0, 0x61, 0x61);
+ try (var r =
CborParser.DEFAULT_NATIVE.getSession().parseTokens(data)) {
+ assertEquals(TokenType.VALUE_STRING, r.next());
+ assertThrows(IndexOutOfBoundsException.class, () ->
r.getTag(1));
+ assertThrows(IndexOutOfBoundsException.class, () ->
r.getTag(-1));
+ }
+ }
+
+ @Test void a10_tagStackGrows() throws Exception {
+ // Five nested tags exceeds the initial tags[] array (size 4)
and must trigger growth.
+ // 0xC0 0xC0 0xC0 0xC0 0xC0 0x05 — 5 tags wrapping uint 5.
+ var data = bytes(0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0x05);
+ try (var r =
CborParser.DEFAULT_NATIVE.getSession().parseTokens(data)) {
+ assertEquals(TokenType.VALUE_NUMBER, r.next());
+ assertEquals(5, r.getTagCount());
+ for (var i = 0; i < 5; i++)
+ assertEquals(0L, r.getTag(i));
+ }
+ }
+
+ //
=================================================================================
+ // B. Writer native-mode (serializeTokens) — round-trip through reader
+ //
=================================================================================
+
+ @Test void b01_writeTaggedString() throws Exception {
+ var bos = new ByteArrayOutputStream();
+ try (var w =
CborSerializer.DEFAULT.getSession().serializeTokens(bos)) {
+ w.writeTag(0);
+ w.string("hello");
+ }
+ try (var r =
CborParser.DEFAULT_NATIVE.getSession().parseTokens(bos.toByteArray())) {
+ assertEquals(TokenType.VALUE_STRING, r.next());
+ assertEquals(BinaryNativeKind.CBOR_TAG,
r.getNativeKind());
+ assertEquals(1, r.getTagCount());
+ assertEquals(0L, r.getTag(0));
+ assertEquals("hello", r.getString());
+ }
+ }
+
+ @Test void b02_writeNestedTags() throws Exception {
+ var bos = new ByteArrayOutputStream();
+ try (var w =
CborSerializer.DEFAULT.getSession().serializeTokens(bos)) {
+ w.writeTag(1);
+ w.writeTag(2);
+ w.number(5L);
+ }
+ try (var r =
CborParser.DEFAULT_NATIVE.getSession().parseTokens(bos.toByteArray())) {
+ assertEquals(TokenType.VALUE_NUMBER, r.next());
+ assertEquals(2, r.getTagCount());
+ assertEquals(1L, r.getTag(0));
+ assertEquals(2L, r.getTag(1));
+ assertEquals(5L, r.getNumber().longValue());
+ }
+ }
+
+ @Test void b03_writeSimple() throws Exception {
+ var bos = new ByteArrayOutputStream();
+ try (var w =
CborSerializer.DEFAULT.getSession().serializeTokens(bos)) {
+ w.writeSimple(16);
+ }
+ try (var r =
CborParser.DEFAULT_NATIVE.getSession().parseTokens(bos.toByteArray())) {
+ assertEquals(TokenType.VALUE_NULL, r.next());
+ assertEquals(BinaryNativeKind.CBOR_SIMPLE,
r.getNativeKind());
+ assertEquals(16, r.getSimpleValue());
+ }
+ }
+
+ @Test void b04_taggedInsideMap() throws Exception {
+ // Tag a map value: { "k": tag(0)("v") }.
+ var bos = new ByteArrayOutputStream();
+ try (var w =
CborSerializer.DEFAULT.getSession().serializeTokens(bos)) {
+ w.startObject();
+ w.fieldName("k");
+ w.writeTag(0);
+ w.string("v");
+ w.endObject();
+ }
+ try (var r =
CborParser.DEFAULT_NATIVE.getSession().parseTokens(bos.toByteArray())) {
+ assertEquals(TokenType.START_OBJECT, r.next());
+ assertEquals(TokenType.FIELD_NAME, r.next());
+ assertEquals("k", r.getFieldName());
+ assertEquals(TokenType.VALUE_STRING, r.next());
+ assertEquals(BinaryNativeKind.CBOR_TAG,
r.getNativeKind());
+ assertEquals(1, r.getTagCount());
+ assertEquals(0L, r.getTag(0));
+ assertEquals("v", r.getString());
+ assertEquals(TokenType.END_OBJECT, r.next());
+ }
+ }
+
+ @Test void b05_taggedInsideArray() throws Exception {
+ // Tag an array element: [ tag(7)(42) ].
+ var bos = new ByteArrayOutputStream();
+ try (var w =
CborSerializer.DEFAULT.getSession().serializeTokens(bos)) {
+ w.startArray();
+ w.writeTag(7);
+ w.number(42L);
+ w.endArray();
+ }
+ try (var r =
CborParser.DEFAULT_NATIVE.getSession().parseTokens(bos.toByteArray())) {
+ assertEquals(TokenType.START_ARRAY, r.next());
+ assertEquals(TokenType.VALUE_NUMBER, r.next());
+ assertEquals(7L, r.getTag(0));
+ assertEquals(42L, r.getNumber().longValue());
+ assertEquals(TokenType.END_ARRAY, r.next());
+ }
+ }
+
+ @Test void b06_extWriteUnsupported() throws Exception {
+ // CBOR doesn't support MsgPack ext; the inherited
default-throwing writeExt should fire.
+ var bos = new ByteArrayOutputStream();
+ try (var w =
CborSerializer.DEFAULT.getSession().serializeTokens(bos)) {
+ assertThrows(UnsupportedOperationException.class, () ->
w.writeExt(0, new byte[]{1}));
+ }
+ }
+
+ //
=================================================================================
+ // C. Convenience-class delegation (CborParser/CborSerializer DEFAULT)
+ //
=================================================================================
+
+ @Test void c01_convenienceDelegation() throws Exception {
+ // CborParser.DEFAULT_NATIVE.parseTokens(...) must yield the
same shape as session-level.
+ var data = bytes(0xC0, 0x05);
+ try (var r = CborParser.DEFAULT_NATIVE.parseTokens(data)) {
+ assertEquals(TokenType.VALUE_NUMBER, r.next());
+ assertEquals(BinaryNativeKind.CBOR_TAG,
r.getNativeKind());
+ assertEquals(1, r.getTagCount());
+ assertEquals(0L, r.getTag(0));
+ }
+ // And the writer side: serializeTokens via DEFAULT.
+ var bos = new ByteArrayOutputStream();
+ try (var w = CborSerializer.DEFAULT.serializeTokens(bos)) {
+ w.writeTag(0);
+ w.number(5L);
+ }
+ assertArrayEquals(data, bos.toByteArray());
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/cbor/CborOutputStream_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/cbor/CborOutputStream_Test.java
index 12bb8db764..91c09de380 100644
---
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/cbor/CborOutputStream_Test.java
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/cbor/CborOutputStream_Test.java
@@ -235,4 +235,23 @@ class CborOutputStream_Test extends TestBase {
var b = CborSerializer.DEFAULT.serialize(m);
assertTrue(toSpacedHex(b).startsWith("B8 18"));
}
+
+ @Test
+ @SuppressWarnings("resource") // writeSimple returns the stream (fluent
this); the discarded value is os, closed by the try-with-resources.
+ void a32_writeSimple() throws Exception {
+ // Major type 7 with the additional info encoding the simple
value.
+ // Inline (0..23): one byte, 0xE0 | value.
+ var bos = new java.io.ByteArrayOutputStream();
+ try (var os = new CborOutputStream(bos)) {
+ os.writeSimple(0);
+ }
+ assertEquals("E0", toSpacedHex(bos.toByteArray()));
+
+ // Two-byte encoding (24..255): 0xF8 then the value.
+ bos = new java.io.ByteArrayOutputStream();
+ try (var os = new CborOutputStream(bos)) {
+ os.writeSimple(255);
+ }
+ assertEquals("F8 FF", toSpacedHex(bos.toByteArray()));
+ }
}
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/msgpack/MsgPackNativeTokenStream_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/msgpack/MsgPackNativeTokenStream_Test.java
new file mode 100644
index 0000000000..815e2a0674
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/msgpack/MsgPackNativeTokenStream_Test.java
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.marshall.msgpack;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.marshall.stream.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Unit tests for the MsgPack opt-in binary-native token surface (175ad).
Verifies that
+ * {@link MsgPackParserSession#parseNativeTokens(Object) parseNativeTokens}
surfaces the {@code ext}
+ * type byte as token-level metadata, and that the default
+ * {@link MsgPackParserSession#parseTokens(Object) parseTokens} path still
drops it.
+ */
+@SuppressWarnings({
+ "resource" // Token readers are closed via try-with-resources; JDT's
flow analysis over chained factory calls yields false-positive leak reports.
+})
+class MsgPackNativeTokenStream_Test extends TestBase {
+
+ private static byte[] ext(int type, byte... payload) throws IOException
{
+ var bos = new ByteArrayOutputStream();
+ try (var os = new MsgPackOutputStream(bos)) {
+ os.writeExt(type, payload);
+ }
+ return bos.toByteArray();
+ }
+
+ //
=================================================================================
+ // A. Reader native-mode (parseNativeTokens)
+ //
=================================================================================
+
+ @Test void a01_ext() throws Exception {
+ // fixext4 with type 5 and 4-byte payload.
+ var data = ext(5, (byte) 1, (byte) 2, (byte) 3, (byte) 4);
+ try (var r =
MsgPackParser.DEFAULT_NATIVE.getSession().parseTokens(data)) {
+ assertEquals(TokenType.VALUE_BINARY, r.next());
+ assertEquals(BinaryNativeKind.MSGPACK_EXT,
r.getNativeKind());
+ assertEquals(5, r.getExtType());
+ assertArrayEquals(new byte[]{1, 2, 3, 4},
r.getBinary());
+ }
+ }
+
+ @Test void a02_negativeExtType() throws Exception {
+ // Negative ext type (-1) — must round-trip as signed int8.
+ var data = ext(-1, (byte) 0x7F);
+ try (var r =
MsgPackParser.DEFAULT_NATIVE.getSession().parseTokens(data)) {
+ assertEquals(TokenType.VALUE_BINARY, r.next());
+ assertEquals(-1, r.getExtType());
+ }
+ }
+
+ @Test void a03_defaultStillNormalizes() throws Exception {
+ // Same input via plain parseTokens: VALUE_BINARY with no
native metadata, type byte dropped.
+ var data = ext(5, (byte) 1, (byte) 2, (byte) 3, (byte) 4);
+ try (var r = MsgPackParser.DEFAULT.parseTokens(data)) {
+ assertEquals(TokenType.VALUE_BINARY, r.next());
+ assertArrayEquals(new byte[]{1, 2, 3, 4},
r.getBinary());
+ }
+ }
+
+ @Test void a04_tagAccessOnMsgPack() throws Exception {
+ var data = ext(5, (byte) 0);
+ try (var r =
MsgPackParser.DEFAULT_NATIVE.getSession().parseTokens(data)) {
+ assertEquals(TokenType.VALUE_BINARY, r.next());
+ // MsgPack has no tags.
+ assertEquals(0, r.getTagCount());
+ assertThrows(IndexOutOfBoundsException.class, () ->
r.getTag(0));
+ }
+ }
+
+ @Test void a05_simpleValueOnMsgPack() throws Exception {
+ var data = ext(5, (byte) 0);
+ try (var r =
MsgPackParser.DEFAULT_NATIVE.getSession().parseTokens(data)) {
+ assertEquals(TokenType.VALUE_BINARY, r.next());
+ // MsgPack has no CBOR simple values.
+ assertThrows(IllegalStateException.class,
r::getSimpleValue);
+ }
+ }
+
+ @Test void a06_extTypeOutOfStateThrows() throws Exception {
+ // A plain string token has no ext: getExtType must throw.
+ var data = new byte[]{(byte) 0xA1, (byte) 'a'}; // fixstr len
1, "a"
+ try (var r =
MsgPackParser.DEFAULT_NATIVE.getSession().parseTokens(data)) {
+ assertEquals(TokenType.VALUE_STRING, r.next());
+ assertThrows(IllegalStateException.class,
r::getExtType);
+ }
+ }
+
+ @Test void a08_largeExt() throws Exception {
+ // 257 bytes uses ext16 framing internally; native-mode must
surface the type/payload.
+ var p = new byte[257];
+ for (var i = 0; i < 257; i++) p[i] = (byte) i;
+ var data = ext(127, p);
+ try (var r =
MsgPackParser.DEFAULT_NATIVE.getSession().parseTokens(data)) {
+ assertEquals(TokenType.VALUE_BINARY, r.next());
+ assertEquals(127, r.getExtType());
+ assertArrayEquals(p, r.getBinary());
+ }
+ }
+
+ //
=================================================================================
+ // B. Writer native-mode (serializeTokens) — round-trip through reader
+ //
=================================================================================
+
+ @Test void b01_writeExt() throws Exception {
+ var bos = new ByteArrayOutputStream();
+ try (var w =
MsgPackSerializer.DEFAULT.getSession().serializeTokens(bos)) {
+ w.writeExt(5, new byte[]{1, 2, 3, 4});
+ }
+ try (var r =
MsgPackParser.DEFAULT_NATIVE.getSession().parseTokens(bos.toByteArray())) {
+ assertEquals(TokenType.VALUE_BINARY, r.next());
+ assertEquals(BinaryNativeKind.MSGPACK_EXT,
r.getNativeKind());
+ assertEquals(5, r.getExtType());
+ assertArrayEquals(new byte[]{1, 2, 3, 4},
r.getBinary());
+ }
+ }
+
+ @Test void b02_extInsideArray() throws Exception {
+ // [ ext(5, [1,2,3,4]) ]
+ var bos = new ByteArrayOutputStream();
+ try (var w =
MsgPackSerializer.DEFAULT.getSession().serializeTokens(bos)) {
+ w.startArray();
+ w.writeExt(5, new byte[]{1, 2, 3, 4});
+ w.endArray();
+ }
+ try (var r =
MsgPackParser.DEFAULT_NATIVE.getSession().parseTokens(bos.toByteArray())) {
+ assertEquals(TokenType.START_ARRAY, r.next());
+ assertEquals(TokenType.VALUE_BINARY, r.next());
+ assertEquals(5, r.getExtType());
+ assertArrayEquals(new byte[]{1, 2, 3, 4},
r.getBinary());
+ assertEquals(TokenType.END_ARRAY, r.next());
+ }
+ }
+
+ @Test void b03_extInsideMap() throws Exception {
+ // { "k": ext(5, [9]) }
+ var bos = new ByteArrayOutputStream();
+ try (var w =
MsgPackSerializer.DEFAULT.getSession().serializeTokens(bos)) {
+ w.startObject();
+ w.fieldName("k");
+ w.writeExt(5, new byte[]{9});
+ w.endObject();
+ }
+ try (var r =
MsgPackParser.DEFAULT_NATIVE.getSession().parseTokens(bos.toByteArray())) {
+ assertEquals(TokenType.START_OBJECT, r.next());
+ assertEquals(TokenType.FIELD_NAME, r.next());
+ assertEquals("k", r.getFieldName());
+ assertEquals(TokenType.VALUE_BINARY, r.next());
+ assertEquals(5, r.getExtType());
+ assertArrayEquals(new byte[]{9}, r.getBinary());
+ assertEquals(TokenType.END_OBJECT, r.next());
+ }
+ }
+
+ @Test void b04_negativeType() throws Exception {
+ var bos = new ByteArrayOutputStream();
+ try (var w =
MsgPackSerializer.DEFAULT.getSession().serializeTokens(bos)) {
+ w.writeExt(-1, new byte[]{0});
+ }
+ try (var r =
MsgPackParser.DEFAULT_NATIVE.getSession().parseTokens(bos.toByteArray())) {
+ assertEquals(TokenType.VALUE_BINARY, r.next());
+ assertEquals(-1, r.getExtType());
+ }
+ }
+
+ @Test void b05_writeTagUnsupported() throws Exception {
+ // MsgPack doesn't support CBOR tags; the inherited
default-throwing writeTag/writeSimple
+ // should fire.
+ var bos = new ByteArrayOutputStream();
+ try (var w =
MsgPackSerializer.DEFAULT.getSession().serializeTokens(bos)) {
+ assertThrows(UnsupportedOperationException.class, () ->
w.writeTag(0));
+ assertThrows(UnsupportedOperationException.class, () ->
w.writeSimple(16));
+ }
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/msgpack/MsgPackOutputStream_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/msgpack/MsgPackOutputStream_Test.java
new file mode 100644
index 0000000000..04e62da511
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/msgpack/MsgPackOutputStream_Test.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.marshall.msgpack;
+
+import static org.apache.juneau.commons.utils.StringUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Low-level MsgPack {@code writeExt} framing verification (fixext / ext8 /
ext16 / ext32 by length).
+ */
+class MsgPackOutputStream_Test extends TestBase {
+
+ @SuppressWarnings("resource") // writeExt returns the stream (fluent
this); the discarded value is os, closed by the try-with-resources.
+ private static String enc(int type, byte[] payload) throws IOException {
+ var bos = new ByteArrayOutputStream();
+ try (var os = new MsgPackOutputStream(bos)) {
+ os.writeExt(type, payload);
+ }
+ return toSpacedHex(bos.toByteArray());
+ }
+
+ @Test
+ void a01_fixext1() throws Exception {
+ assertEquals("D4 05 7F", enc(5, new byte[]{0x7F}));
+ }
+
+ @Test
+ void a02_fixext2() throws Exception {
+ assertEquals("D5 05 01 02", enc(5, new byte[]{1, 2}));
+ }
+
+ @Test
+ void a03_fixext4() throws Exception {
+ assertEquals("D6 05 01 02 03 04", enc(5, new byte[]{1, 2, 3,
4}));
+ }
+
+ @Test
+ void a04_fixext8() throws Exception {
+ assertEquals("D7 05 01 02 03 04 05 06 07 08", enc(5, new
byte[]{1, 2, 3, 4, 5, 6, 7, 8}));
+ }
+
+ @Test
+ void a05_fixext16() throws Exception {
+ var p = new byte[16];
+ for (var i = 0; i < 16; i++) p[i] = (byte) i;
+ assertTrue(enc(5, p).startsWith("D8 05 "));
+ }
+
+ @Test
+ void a06_ext8_threeBytes() throws Exception {
+ // 3 bytes is not a fixext length, so ext8 framing applies.
+ assertEquals("C7 03 05 01 02 03", enc(5, new byte[]{1, 2, 3}));
+ }
+
+ @Test
+ void a07_ext16_257Bytes() throws Exception {
+ var p = new byte[257];
+ var hex = enc(5, p);
+ // C8 = ext16, length is 0x0101, type 0x05, then 257 zero bytes.
+ assertTrue(hex.startsWith("C8 01 01 05 "));
+ }
+
+ @Test
+ void a08_negativeExtType() throws Exception {
+ // Type byte is signed; -1 must serialize as 0xFF.
+ assertEquals("D4 FF 7F", enc(-1, new byte[]{0x7F}));
+ }
+
+ @Test
+ void a09_largeNegativeExtType() throws Exception {
+ // -128 must serialize as 0x80.
+ assertEquals("D4 80 00", enc(-128, new byte[]{0x00}));
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/stream/BinaryNativeKind_GenericConsumer_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/stream/BinaryNativeKind_GenericConsumer_Test.java
new file mode 100644
index 0000000000..0869005595
--- /dev/null
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/stream/BinaryNativeKind_GenericConsumer_Test.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.marshall.stream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.marshall.cbor.*;
+import org.apache.juneau.marshall.msgpack.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Demonstrates that a single format-agnostic consumer can branch on
+ * {@link BinaryNativeKind} alone (without per-format {@code instanceof} on
the cursor) and
+ * extract the native metadata from both CBOR and MsgPack opt-in cursors.
This is the
+ * 175ad base-role abstraction that lets a generic native-aware consumer work
uniformly across
+ * formats.
+ */
+@SuppressWarnings({
+ "resource" // Token readers are closed via try-with-resources; JDT's
flow analysis over chained factory calls yields false-positive leak reports.
+})
+class BinaryNativeKind_GenericConsumer_Test extends TestBase {
+
+ /**
+ * A format-agnostic consumer that walks a {@link TokenReader} in
native mode and renders
+ * each value-bearing token as a string with its native metadata,
branching purely on
+ * {@link BinaryNativeKind}.
+ */
+ private static String render(TokenReader r) throws Exception {
+ var sb = new StringBuilder();
+ while (true) {
+ var t = r.next();
+ if (t == TokenType.END_OF_STREAM)
+ break;
+ switch (t) {
+ case VALUE_NUMBER:
+ case VALUE_STRING:
+ case VALUE_BINARY:
+ case VALUE_NULL:
+ case VALUE_BOOLEAN:
+ sb.append(t).append('(');
+ switch (r.getNativeKind()) {
+ case CBOR_TAG:
+ sb.append("tagged:");
+ for (var i = 0; i <
r.getTagCount(); i++) {
+ if (i > 0)
sb.append(',');
+
sb.append(r.getTag(i));
+ }
+ sb.append(';');
+ break;
+ case CBOR_SIMPLE:
+
sb.append("simple:").append(r.getSimpleValue()).append(';');
+ break;
+ case MSGPACK_EXT:
+
sb.append("ext:").append(r.getExtType()).append(';');
+ break;
+ case NONE:
+ break;
+ }
+ sb.append(')');
+ break;
+ default:
+ sb.append(t);
+ }
+ sb.append(' ');
+ }
+ return sb.toString().trim();
+ }
+
+ @Test void a01_cborTaggedString() throws Exception {
+ // CBOR: tag(0) wrapping "hi"
+ var bos = new ByteArrayOutputStream();
+ try (var w = CborSerializer.DEFAULT.serializeTokens(bos)) {
+ w.writeTag(0);
+ w.string("hi");
+ }
+ try (var r =
CborParser.DEFAULT_NATIVE.parseTokens(bos.toByteArray())) {
+ assertEquals("VALUE_STRING(tagged:0;)", render(r));
+ }
+ }
+
+ @Test void a02_cborSimple() throws Exception {
+ // Use simple value 16 (within the valid 0..19 range; 20-23 are
reserved for
+ // false/true/null/undefined and 25-27 for float16/32/64).
+ var bos = new ByteArrayOutputStream();
+ try (var w = CborSerializer.DEFAULT.serializeTokens(bos)) {
+ w.writeSimple(16);
+ }
+ try (var r =
CborParser.DEFAULT_NATIVE.parseTokens(bos.toByteArray())) {
+ assertEquals("VALUE_NULL(simple:16;)", render(r));
+ }
+ }
+
+ @Test void a03_msgpackExt() throws Exception {
+ var bos = new ByteArrayOutputStream();
+ try (var w = MsgPackSerializer.DEFAULT.serializeTokens(bos)) {
+ w.writeExt(5, new byte[]{1, 2, 3, 4});
+ }
+ try (var r =
MsgPackParser.DEFAULT_NATIVE.parseTokens(bos.toByteArray())) {
+ assertEquals("VALUE_BINARY(ext:5;)", render(r));
+ }
+ }
+}
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/stream/FormatStreamingCapability_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/stream/FormatStreamingCapability_Test.java
index fd040e6125..ea5709e9d1 100644
---
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/stream/FormatStreamingCapability_Test.java
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/stream/FormatStreamingCapability_Test.java
@@ -132,4 +132,5 @@ class FormatStreamingCapability_Test extends TestBase {
}
assertTrue(checked > 0, "expected at least one format to expose
an array-record writer");
}
+
}