From a8a4980734998644a251686bf802dd6c8c472db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Wed, 15 May 2019 19:27:57 +0200 Subject: [PATCH 01/25] Add encodeUnsignedMedium to NumberUtils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Simon Baslé --- .../java/io/rsocket/util/NumberUtils.java | 19 ++++++++++ .../java/io/rsocket/util/NumberUtilsTest.java | 37 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java b/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java index 12e3cee45..61a3f3a62 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java +++ b/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java @@ -16,6 +16,8 @@ package io.rsocket.util; +import io.netty.buffer.ByteBuf; + import java.util.Objects; public final class NumberUtils { @@ -143,4 +145,21 @@ public static int requireUnsignedShort(int i) { return i; } + + /** + * Encode an unsigned medium integer on 3 bytes / 24 bits. This can be decoded directly by the + * {@link ByteBuf#readUnsignedMedium()} method. + * + * @param byteBuf the {@link ByteBuf} into which to write the bits + * @param i the medium integer to encode + * @see #requireUnsignedMedium(int) + */ + public static void encodeUnsignedMedium(ByteBuf byteBuf, int i) { + requireUnsignedMedium(i); + // Write each byte separately in reverse order, this mean we can write 1 << 23 without + // overflowing. + byteBuf.writeByte(i >> 16); + byteBuf.writeByte(i >> 8); + byteBuf.writeByte(i); + } } diff --git a/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java b/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java index 988bd523d..69e88d86c 100644 --- a/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java +++ b/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java @@ -18,6 +18,10 @@ import static org.assertj.core.api.Assertions.*; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.rsocket.test.util.ByteBufUtils; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -158,4 +162,37 @@ void requireUnsignedShortOverFlow() { .isThrownBy(() -> NumberUtils.requireUnsignedShort(1 << 16)) .withMessage("%d is larger than 16 bits", 1 << 16); } + + @Test + void encodeUnsignedMedium() { + ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); + NumberUtils.encodeUnsignedMedium(buffer,129); + buffer.markReaderIndex(); + + assertThat(buffer.readUnsignedMedium()) + .as("reading as unsigned medium") + .isEqualTo(129); + + buffer.resetReaderIndex(); + assertThat(buffer.readMedium()) + .as("reading as signed medium") + .isEqualTo(129); + } + + @Test + void encodeUnsignedMediumLarge() { + ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); + NumberUtils.encodeUnsignedMedium(buffer, 0xFFFFFC); + buffer.markReaderIndex(); + + assertThat(buffer.readUnsignedMedium()) + .as("reading as unsigned medium") + .isEqualTo(16777212); + + buffer.resetReaderIndex(); + assertThat(buffer.readMedium()) + .as("reading as signed medium") + .isEqualTo(-4); + } + } From 1f3a7d7afdfb3ca1f641c1845c6fed522ffd9659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Wed, 15 May 2019 19:35:44 +0200 Subject: [PATCH 02/25] Add WellKnownMimeType enum with String/ID conversions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Simon Baslé --- .../rsocket/metadata/WellKnownMimeType.java | 119 ++++++++++++++++++ .../metadata/WellKnownMimeTypeTest.java | 53 ++++++++ 2 files changed, 172 insertions(+) create mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java create mode 100644 rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java new file mode 100644 index 000000000..59a2adccc --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java @@ -0,0 +1,119 @@ +package io.rsocket.metadata; + +/** + * Enumeration of Well Known Mime Types, as defined in the eponymous extension. Such mime types + * are used in composite metadata (which can include routing and/or tracing metadata). + */ +public enum WellKnownMimeType { + + APPLICATION_AVRO("application/avro", (byte)0), + APPLICATION_CBOR("application/cbor", (byte)1), + APPLICATION_GRAPHQL("application/graphql", (byte)2), + APPLICATION_GZIP("application/gzip", (byte)3), + APPLICATION_JAVASCRIPT("application/javascript", (byte)4), + APPLICATION_JSON("application/json", (byte)5), + APPLICATION_OCTET_STREAM("application/octet-stream", (byte)6), + APPLICATION_PDF("application/pdf", (byte)7), + APPLICATION_THRIFT("application/vnd.apache.thrift.binary", (byte) 8), + APPLICATION_PROTOBUF("application/vnd.google.protobuf", (byte)9), + APPLICATION_XML("application/xml", (byte)10), + APPLICATION_ZIP("application/zip", (byte)11), + AUDIO_AAC("audio/aac", (byte)12), + AUDIO_MP3("audio/mp3", (byte)13), + AUDIO_MP4("audio/mp4", (byte)14), + AUDIO_MPEG3("audio/mpeg3", (byte)15), + AUDIO_MPEG("audio/mpeg", (byte)16), + AUDIO_OGG("audio/ogg", (byte)17), + AUDIO_OPUS("audio/opus", (byte)18), + AUDIO_VORBIS("audio/vorbis", (byte)19), + IMAGE_BMP("image/bmp", (byte)20), + IMAGE_GIG("image/gif", (byte)21), + IMAGE_HEIC_SEQUENCE("image/heic-sequence", (byte)22), + IMAGE_HEIC("image/heic", (byte)23), + IMAGE_HEIF_SEQUENCE("image/heif-sequence", (byte)24), + IMAGE_HEIF("image/heif", (byte)25), + IMAGE_JPEG("image/jpeg", (byte)26), + IMAGE_PNG("image/png", (byte)27), + IMAGE_TIFF("image/tiff", (byte)28), + MULTIPART_MIXED("multipart/mixed", (byte)29), + TEXT_CSS("text/css", (byte)30), + TEXT_CSV("text/csv", (byte)31), + TEXT_HTML("text/html", (byte)32), + TEXT_PLAIN("text/plain", (byte)33), + TEXT_XML("text/xml", (byte)34), + VIDEO_H264("video/H264", (byte)35), + VIDEO_H265("video/H265", (byte)36), + VIDEO_VP8("video/VP8", (byte)37), + //... reserved for future use ... + MESSAGE_RSOCKET_TRACING_ZIPKIN("message/x.rsocket.tracing-zipkin.v0", (byte)125), + MESSAGE_RSOCKET_ROUTING("message/x.rsocket.routing.v0", (byte)126), + MESSAGE_RSOCKET_COMPOSITE_METADATA("message/x.rsocket.composite-metadata.v0", (byte)127); + + private final String str; + private final byte identifier; + + WellKnownMimeType(String str, byte identifier) { + if (identifier < 0) throw new IllegalArgumentException("identifier must be between 0 and 127, inclusive"); + this.str = str; + this.identifier = identifier; + } + + /** + * @return the byte identifier of the mime type, guaranteed to be positive or zero. + */ + public byte getIdentifier() { + return identifier; + } + + /** + * @return the mime type represented as a {@link String}, which is made of US_ASCII compatible characters only + */ + public String getMime() { + return str; + } + + /** + * @see #getMime() + */ + @Override + public String toString() { + return str; + } + + /** + * Find the {@link WellKnownMimeType} for the given {@link String} representation. If the representation if + * {@code null} or doesn't match a {@link WellKnownMimeType}, an {@link IllegalArgumentException} is thrown. + * @param mimeType the looked up mime type + * @return the {@link WellKnownMimeType} + * @throws IllegalArgumentException if the requested type is unknown or null + */ + public static WellKnownMimeType fromMimeType(String mimeType) { + if (mimeType == null) throw new IllegalArgumentException("type must be non-null"); + for (WellKnownMimeType value : values()) { + if (mimeType.equals(value.str)) { + return value; + } + } + throw new IllegalArgumentException("not a WellKnownMimeType: " + mimeType); + } + + /** + * Find the {@link WellKnownMimeType} for the given ID (as an int). Valid IDs are defined to be integers between 0 + * and 127, inclusive. However some IDs in that are still only reserved and don't have a type associated yet: this + * method throws an {@link IllegalStateException} when passing such a ID. + * + * @param id the looked up id + * @return the {@link WellKnownMimeType} + * @throws IllegalArgumentException if the requested id is out of the specification's range + * @throws IllegalStateException if the requested id is one that is merely reserved + */ + public static WellKnownMimeType fromId(int id) { + if (id < 0 || id > 127) { + throw new IllegalArgumentException("WellKnownMimeType IDs are between 0 and 127, inclusive"); + } + for (WellKnownMimeType value : values()) { + if (value.getIdentifier() == id) return value; + } + throw new IllegalStateException(id + " is between 0 and 127 yet no WellKnownMimeType found"); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java new file mode 100644 index 000000000..ded385011 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java @@ -0,0 +1,53 @@ +package io.rsocket.metadata; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +/** + * @author Simon Baslé + */ +class WellKnownMimeTypeTest { + + @Test + void fromIdMatchFromMimeType() { + for (WellKnownMimeType mimeType : WellKnownMimeType.values()) { + assertThat(WellKnownMimeType.fromMimeType(mimeType.toString())) + .as("mimeType string for " + mimeType.name()) + .isSameAs(mimeType); + + assertThat(WellKnownMimeType.fromId(mimeType.getIdentifier())) + .as("mimeType ID for " + mimeType.name()) + .isSameAs(mimeType); + } + } + + @Test + void fromIdNegative() { + assertThatIllegalArgumentException().isThrownBy(() -> + WellKnownMimeType.fromId(-1)) + .withMessage("WellKnownMimeType IDs are between 0 and 127, inclusive"); + } + + @Test + void fromIdGreaterThan127() { + assertThatIllegalArgumentException().isThrownBy(() -> + WellKnownMimeType.fromId(128)) + .withMessage("WellKnownMimeType IDs are between 0 and 127, inclusive"); + } + + @Test + void fromIdReserved() { + assertThatIllegalStateException().isThrownBy(() -> + WellKnownMimeType.fromId(120)) + .withMessage("120 is between 0 and 127 yet no WellKnownMimeType found"); + } + + @Test + void fromMimeTypeUnknown() { + assertThatIllegalArgumentException().isThrownBy(() -> + WellKnownMimeType.fromMimeType("foo/bar")) + .withMessage("not a WellKnownMimeType: foo/bar"); + } + +} \ No newline at end of file From 772d0eba5a9e4d885eaad6b6b400f5da75920572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Wed, 15 May 2019 19:36:25 +0200 Subject: [PATCH 03/25] Add a flyweight capable of encoding Composite Metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flyweight also exposes a method to decode Composite Metadata to a Map. Signed-off-by: Simon Baslé --- .../frame/StreamMetadataFlyweight.java | 170 +++++++++ .../frame/StreamMetadataFlyweightTest.java | 327 ++++++++++++++++++ 2 files changed, 497 insertions(+) create mode 100644 rsocket-core/src/main/java/io/rsocket/frame/StreamMetadataFlyweight.java create mode 100644 rsocket-core/src/test/java/io/rsocket/frame/StreamMetadataFlyweightTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/frame/StreamMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/StreamMetadataFlyweight.java new file mode 100644 index 000000000..af86d79d8 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/StreamMetadataFlyweight.java @@ -0,0 +1,170 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.netty.util.CharsetUtil; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.util.NumberUtils; + +import java.util.HashMap; +import java.util.Map; + +class StreamMetadataFlyweight { + + + private static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 + private static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 + + private StreamMetadataFlyweight() {} + + /** + * Decode the next mime type information from a composite metadata buffer which {@link ByteBuf#readerIndex()} is + * at the start of the next metadata section. + *

+ * Mime type is returned as a {@link String} containing only US_ASCII characters, and the index is moved past the + * mime section, to the starting byte of the sub-metadata's length. + * + * @param buffer the metadata or composite metadata to read mime information from. + * @return the next metadata mime type as {@link String}. the buffer {@link ByteBuf#readerIndex()} is moved. + */ + static String decodeMimeFromMetadataHeader(ByteBuf buffer) { + byte source = buffer.readByte(); + if ((source & STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK) { + //M flag set + int id = source & STREAM_METADATA_LENGTH_MASK; + WellKnownMimeType mime = WellKnownMimeType.fromId(id); + if (mime != null) { + return mime.toString(); + } + else { + throw new IllegalStateException(Integer.toBinaryString(source) + " is not an expected binary representation"); + } + + } + //M flag unset, remaining 7 bits are the length of the mime + int mimeLength = Byte.toUnsignedInt(source) + 1; + CharSequence mime = buffer.readCharSequence(mimeLength, CharsetUtil.US_ASCII); + return mime.toString(); + } + + /** + * Decode the current metadata length information from a composite metadata buffer which {@link ByteBuf#readerIndex()} + * is just past the current metadata section's mime information. + *

+ * The index is moved past the metadata length section, to the starting byte of the current metadata's value. + * + * @param buffer the metadata or composite metadata to read length information from. + * @return the next metadata length. the buffer {@link ByteBuf#readerIndex()} is moved. + */ + static int decodeMetadataLengthFromMetadataHeader(ByteBuf buffer) { + if (buffer.readableBytes() < 3) { + throw new IllegalStateException("the given buffer should contain at least 3 readable bytes after decoding mime type"); + } + return buffer.readUnsignedMedium(); + } + + /** + * Encode a {@link WellKnownMimeType well known mime type} and a metadata value length into a newly allocated + * {@link ByteBuf}. + *

+ * This compact representation encodes the mime type via its ID on a single byte, and the unsigned value length on + * 3 additional bytes. + * + * @param allocator the {@link ByteBufAllocator} to use to create the buffer. + * @param mimeType a {@link WellKnownMimeType} to encode. + * @param metadataLength the metadata length to append to the buffer as an unsigned 24 bits integer. + * @return the encoded mime and metadata length information + */ + static ByteBuf encodeMetadataHeader(ByteBufAllocator allocator, WellKnownMimeType mimeType, int metadataLength) { + ByteBuf buffer = allocator.buffer(4, 4) + .writeByte(mimeType.getIdentifier() | STREAM_METADATA_KNOWN_MASK); + + NumberUtils.encodeUnsignedMedium(buffer, metadataLength); + + return buffer; + } + + /** + * Encode a custom mime type and a metadata value length into a newly allocated {@link ByteBuf}. + *

+ * This larger representation encodes the mime type representation's length on a single byte, then the representation + * itself, then the unsigned metadata value length on 3 additional bytes. + * + * @param allocator the {@link ByteBufAllocator} to use to create the buffer. + * @param customMime a custom mime type to encode. + * @param metadataLength the metadata length to append to the buffer as an unsigned 24 bits integer. + * @return the encoded mime and metadata length information + */ + static ByteBuf encodeMetadataHeader(ByteBufAllocator allocator, String customMime, int metadataLength) { + ByteBuf mimeBuffer = allocator.buffer(customMime.length()); + mimeBuffer.writeCharSequence(customMime, CharsetUtil.UTF_8); + if (!ByteBufUtil.isText(mimeBuffer, CharsetUtil.US_ASCII)) { + throw new IllegalArgumentException("custom mime type must be US_ASCII characters only"); + } + int ml = mimeBuffer.readableBytes(); + if (ml < 1 || ml > 128) { + throw new IllegalArgumentException("custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + + ByteBuf mimeLength = allocator.buffer(1,1); + mimeLength.writeByte(ml - 1); + + ByteBuf metadataLengthBuffer = allocator.buffer(3, 3); + NumberUtils.encodeUnsignedMedium(metadataLengthBuffer, metadataLength); + + return allocator.compositeBuffer() + .addComponents(true, mimeLength, mimeBuffer, metadataLengthBuffer); + } + + /** + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf buffer}. + * + * @param compositeMetaData the buffer that will hold all composite metadata information. + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param customMimeType the custom mime type to encode. + * @param metadata the metadata value to encode. + * @see #encodeMetadataHeader(ByteBufAllocator, String, int) + */ + static void addMetadata(CompositeByteBuf compositeMetaData, ByteBufAllocator allocator, String customMimeType, ByteBuf metadata) { + compositeMetaData.addComponents(true, + encodeMetadataHeader(allocator, customMimeType, metadata.readableBytes()), + metadata); + } + + /** + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf buffer}. + * + * @param compositeMetaData the buffer that will hold all composite metadata information. + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param knownMimeType the {@link WellKnownMimeType} to encode. + * @param metadata the metadata value to encode. + * @see #encodeMetadataHeader(ByteBufAllocator, WellKnownMimeType, int) + */ + static void addMetadata(CompositeByteBuf compositeMetaData, ByteBufAllocator allocator, WellKnownMimeType knownMimeType, ByteBuf metadata) { + compositeMetaData.addComponents(true, + encodeMetadataHeader(allocator, knownMimeType, metadata.readableBytes()), + metadata); + } + + /** + * Decode composite metadata information into a {@link Map} of {@link String} mime types to {@link ByteBuf} metadata + * values. + * + * @param compositeMetadata the {@link ByteBuf} that contains information for one or more metadata mime-value pairs. + * @param retainMetadataSlices should metadata value {@link ByteBuf} be {@link ByteBuf#retain() retained} when decoded? + * @return the decoded composite metadata + */ + public static Map decodeToMap(ByteBuf compositeMetadata, boolean retainMetadataSlices) { + Map map = new HashMap<>(); + while (compositeMetadata.isReadable()) { + String mime = decodeMimeFromMetadataHeader(compositeMetadata); + int length = decodeMetadataLengthFromMetadataHeader(compositeMetadata); + + ByteBuf metadata = retainMetadataSlices ? compositeMetadata.readRetainedSlice(length) : compositeMetadata.readSlice(length); + map.put(mime, metadata); + } + return map; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/StreamMetadataFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/StreamMetadataFlyweightTest.java new file mode 100644 index 000000000..1df551f35 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/StreamMetadataFlyweightTest.java @@ -0,0 +1,327 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.netty.util.CharsetUtil; +import io.rsocket.metadata.WellKnownMimeType; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class StreamMetadataFlyweightTest { + + static String toHeaderBits(ByteBuf encoded) { + encoded.markReaderIndex(); + byte headerByte = encoded.readByte(); + String byteAsString = String.format("%8s", Integer.toBinaryString(headerByte & 0xFF)).replace(' ', '0'); + encoded.resetReaderIndex(); + return byteAsString; + } + // ==== + + @Test + void knownMimeHeaderZero_avro() { + WellKnownMimeType mime = WellKnownMimeType.APPLICATION_AVRO; + assertThat(mime.getIdentifier()).as("AVRO identifier").isZero(); + ByteBuf encoded = StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime, 0); + + assertThat(toHeaderBits(encoded)) + .startsWith("1") + .isEqualTo("10000000"); + + String decoded = StreamMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); + + assertThat(decoded).isEqualTo(mime.toString()); + } + + @Test + void knownMimeHeader127_compositeMetadata() { + WellKnownMimeType mime = WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA; + assertThat(mime.getIdentifier()).as("COMPOSITE METADATA identifier").isEqualTo((byte) 127); + ByteBuf encoded = StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime, 0); + + assertThat(toHeaderBits(encoded)) + .startsWith("1") + .isEqualTo("11111111"); + + String decoded = StreamMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); + + assertThat(decoded).isEqualTo(mime.toString()); + } + + @Test + void customMimeHeaderLengthOne() { + String mimeString ="w"; + ByteBuf encoded = StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + + assertThat(toHeaderBits(encoded)) + .startsWith("0") + .isEqualTo("00000000"); + + String decoded = StreamMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); + + assertThat(decoded).isEqualTo(mimeString); + } + + @Test + void customMimeHeaderLength127() { + StringBuilder builder = new StringBuilder(127); + for (int i = 0; i < 127; i++) { + builder.append('a'); + } + String longString = builder.toString(); + ByteBuf encoded = StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, longString, 0); + + assertThat(toHeaderBits(encoded)) + .startsWith("0") + .isEqualTo("01111110"); + + String decoded = StreamMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); + + assertThat(decoded).isEqualTo(longString); + } + + @Test + void customMimeHeaderLength128() { + StringBuilder builder = new StringBuilder(128); + for (int i = 0; i < 128; i++) { + builder.append('a'); + } + String longString = builder.toString(); + ByteBuf encoded = StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, longString, 0); + + assertThat(toHeaderBits(encoded)) + .startsWith("0") + .isEqualTo("01111111"); + + String decoded = StreamMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); + + assertThat(decoded).isEqualTo(longString); + } + + @Test + void customMimeHeaderLength129_encodingFails() { + StringBuilder builder = new StringBuilder(129); + for (int i = 0; i < 129; i++) { + builder.append('a'); + } + + assertThatIllegalArgumentException() + .isThrownBy(() -> StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, builder.toString(), 0)) + .withMessage("custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + + @Test + void customMimeHeaderNonAscii_encodingFails() { + String mimeNotAscii = "mime/typé"; + + assertThatIllegalArgumentException() + .isThrownBy(() -> StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeNotAscii, 0)) + .withMessage("custom mime type must be US_ASCII characters only"); + } + + @Test + void customMimeHeaderLength0_encodingFails() { + assertThatIllegalArgumentException() + .isThrownBy(() -> StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "", 0)) + .withMessage("custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + + @Test + void decodeMetadataLengthFromUntouchedWithKnownMime() { + ByteBuf encoded = StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, WellKnownMimeType.APPLICATION_GZIP, 12); + + assertThat(StreamMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)) + .withFailMessage("should not correctly decode if not at correct reader index") + .isNotEqualTo(12); + } + + @Test + void decodeMetadataLengthFromMimeDecodedWithKnownMime() { + ByteBuf encoded = StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, WellKnownMimeType.APPLICATION_GZIP, 12); + StreamMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); + + assertThat(StreamMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)).isEqualTo(12); + } + + @Test + void decodeMetadataLengthFromUntouchedWithCustomMime() { + ByteBuf encoded = StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo/bar", 12); + + assertThat(StreamMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)) + .withFailMessage("should not correctly decode if not at correct reader index") + .isNotEqualTo(12); + } + + @Test + void decodeMetadataLengthFromMimeDecodedWithCustomMime() { + ByteBuf encoded = StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo/bar", 12); + StreamMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); + + assertThat(StreamMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)).isEqualTo(12); + } + + @Test + void compositeMetadata() { + //metadata 1: + WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; + ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); + metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); + int metadataLength1 = metadata1.readableBytes(); + + //metadata 2: + String mimeType2 = "application/custom"; + ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); + metadata2.writeChar('E'); + metadata2.writeChar('∑'); + metadata2.writeChar('é'); + metadata2.writeBoolean(true); + metadata2.writeChar('W'); + int metadataLength2 = metadata2.readableBytes(); + + CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + StreamMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); + StreamMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); + System.out.println(ByteBufUtil.prettyHexDump(compositeMetadata)); + + compositeMetadata.readByte(); //ignore the "know mime + ID" byte for now + assertThat(compositeMetadata.readUnsignedMedium()) + .as("metadata1 length") + .isEqualTo(metadataLength1); + assertThat(compositeMetadata.readCharSequence(metadataLength1, CharsetUtil.UTF_8)) + .as("metadata1 value").isEqualTo("abcdefghijkl"); + + int mimeLength = compositeMetadata.readByte() + 1; + + assertThat(compositeMetadata.readCharSequence(mimeLength, CharsetUtil.US_ASCII).toString()) + .as("metadata2 custom mime ") + .isEqualTo(mimeType2); + assertThat(compositeMetadata.readUnsignedMedium()) + .as("metadata2 length") + .isEqualTo(metadataLength2); + assertThat(compositeMetadata.readChar()) + .as("metadata2 value 1/5") + .isEqualTo('E'); + assertThat(compositeMetadata.readChar()) + .as("metadata2 value 2/5") + .isEqualTo('∑'); + + assertThat(compositeMetadata.readChar()) + .as("metadata2 value 3/5") + .isEqualTo('é'); + assertThat(compositeMetadata.readBoolean()) + .as("metadata2 value 4/5") + .isTrue(); + assertThat(compositeMetadata.readChar()) + .as("metadata2 value 5/5") + .isEqualTo('W'); + + assertThat(compositeMetadata.readableBytes()) + .as("reading composite metadata done") + .isZero(); + } + + @Test + void decodeCompositeMetadata() { + //metadata 1: + WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; + ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); + metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); + + //metadata 2: + String mimeType2 = "application/custom"; + ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); + metadata2.writeChar('E'); + metadata2.writeChar('∑'); + metadata2.writeChar('é'); + metadata2.writeBoolean(true); + metadata2.writeChar('W'); + + CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + StreamMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); + StreamMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); + + Map decoded = StreamMetadataFlyweight.decodeToMap(compositeMetadata, false); + + assertThat(decoded) + .as("decoded keys") + .containsOnlyKeys(WellKnownMimeType.APPLICATION_PDF.getMime(), "application/custom"); + + ByteBuf decoded1 = decoded.get(WellKnownMimeType.APPLICATION_PDF.getMime()); + ByteBuf decoded2 = decoded.get("application/custom"); + + assertThat(decoded1.toString(CharsetUtil.UTF_8)) + .as("metadata1 decoded") + .isEqualTo("abcdefghijkl"); + + System.out.println(ByteBufUtil.hexDump(decoded2)); + + assertThat(decoded2) + .as("metadata2 decoded") + .isEqualByComparingTo(metadata2); + } + + @Test + void decodeCompositeMetadataRetainSlices() { + //metadata 1: + WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; + ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); + metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); + + //metadata 2: + String mimeType2 = "application/custom"; + ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); + metadata2.writeChar('E'); + metadata2.writeChar('∑'); + metadata2.writeChar('é'); + metadata2.writeBoolean(true); + metadata2.writeChar('W'); + + CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + StreamMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); + StreamMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); + + Map decoded = StreamMetadataFlyweight.decodeToMap(compositeMetadata, true); + + assertThat(decoded).allSatisfy((key, buf) -> + assertThat(buf.refCnt()) + .as("metadata buffer retained for " + key) + .isGreaterThan(1) + ); + } + + @Test + void decodeCompositeMetadataNoRetainSlices() { + //metadata 1: + WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; + ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); + metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); + + //metadata 2: + String mimeType2 = "application/custom"; + ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); + metadata2.writeChar('E'); + metadata2.writeChar('∑'); + metadata2.writeChar('é'); + metadata2.writeBoolean(true); + metadata2.writeChar('W'); + + CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + StreamMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); + StreamMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); + + Map decoded = StreamMetadataFlyweight.decodeToMap(compositeMetadata, false); + + assertThat(decoded).allSatisfy((key, buf) -> + assertThat(buf.refCnt()) + .as("metadata buffer not retained for " + key) + .isOne() + ); + } + +} \ No newline at end of file From 427fcf599105701e800c6ca7fdafd139fe77854a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 16 May 2019 17:33:03 +0200 Subject: [PATCH 04/25] Move flyweight into metadata package, change decodeToMap to decodeNext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Simon Baslé --- .../CompositeMetadataFlyweight.java} | 33 ++-- .../CompositeMetadataFlyweightTest.java} | 153 ++++++++++-------- 2 files changed, 106 insertions(+), 80 deletions(-) rename rsocket-core/src/main/java/io/rsocket/{frame/StreamMetadataFlyweight.java => metadata/CompositeMetadataFlyweight.java} (87%) rename rsocket-core/src/test/java/io/rsocket/{frame/StreamMetadataFlyweightTest.java => metadata/CompositeMetadataFlyweightTest.java} (60%) diff --git a/rsocket-core/src/main/java/io/rsocket/frame/StreamMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java similarity index 87% rename from rsocket-core/src/main/java/io/rsocket/frame/StreamMetadataFlyweight.java rename to rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java index af86d79d8..0bb039ff0 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/StreamMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java @@ -1,23 +1,19 @@ -package io.rsocket.frame; +package io.rsocket.metadata; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.CompositeByteBuf; import io.netty.util.CharsetUtil; -import io.rsocket.metadata.WellKnownMimeType; import io.rsocket.util.NumberUtils; -import java.util.HashMap; -import java.util.Map; - -class StreamMetadataFlyweight { +class CompositeMetadataFlyweight { private static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 private static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 - private StreamMetadataFlyweight() {} + private CompositeMetadataFlyweight() {} /** * Decode the next mime type information from a composite metadata buffer which {@link ByteBuf#readerIndex()} is @@ -149,22 +145,25 @@ static void addMetadata(CompositeByteBuf compositeMetaData, ByteBufAllocator all } /** - * Decode composite metadata information into a {@link Map} of {@link String} mime types to {@link ByteBuf} metadata - * values. + * Decode the next composite metadata piece from a composite metadata buffer into an {@link Object} array which + * holds two elements: the {@link String} mime types and the {@link ByteBuf} metadata value. + * The array is empty if the composite metadata buffer has been entirely decoded, but generally this method shouldn't + * be called if the buffer's {@link ByteBuf#isReadable()} method returns {@code false}. * - * @param compositeMetadata the {@link ByteBuf} that contains information for one or more metadata mime-value pairs. + * @param compositeMetadata the {@link ByteBuf} that contains information for one or more metadata mime-value pairs, + * with its reader index set at the start of next metadata piece (or end of the buffer if + * fully decoded) * @param retainMetadataSlices should metadata value {@link ByteBuf} be {@link ByteBuf#retain() retained} when decoded? - * @return the decoded composite metadata + * @return the decoded piece of composite metadata, or an empty Object array if no more metadata is in the composite */ - public static Map decodeToMap(ByteBuf compositeMetadata, boolean retainMetadataSlices) { - Map map = new HashMap<>(); - while (compositeMetadata.isReadable()) { + static Object[] decodeNext(ByteBuf compositeMetadata, boolean retainMetadataSlices) { + if (compositeMetadata.isReadable()) { String mime = decodeMimeFromMetadataHeader(compositeMetadata); int length = decodeMetadataLengthFromMetadataHeader(compositeMetadata); - ByteBuf metadata = retainMetadataSlices ? compositeMetadata.readRetainedSlice(length) : compositeMetadata.readSlice(length); - map.put(mime, metadata); + + return new Object[] {mime, metadata}; } - return map; + return new Object[0]; } } diff --git a/rsocket-core/src/test/java/io/rsocket/frame/StreamMetadataFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java similarity index 60% rename from rsocket-core/src/test/java/io/rsocket/frame/StreamMetadataFlyweightTest.java rename to rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java index 1df551f35..42495ac89 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/StreamMetadataFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java @@ -1,19 +1,19 @@ -package io.rsocket.frame; +package io.rsocket.metadata; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.CompositeByteBuf; import io.netty.util.CharsetUtil; -import io.rsocket.metadata.WellKnownMimeType; import org.junit.jupiter.api.Test; -import java.util.Map; +import java.util.ArrayList; +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -class StreamMetadataFlyweightTest { +class CompositeMetadataFlyweightTest { static String toHeaderBits(ByteBuf encoded) { encoded.markReaderIndex(); @@ -28,13 +28,13 @@ static String toHeaderBits(ByteBuf encoded) { void knownMimeHeaderZero_avro() { WellKnownMimeType mime = WellKnownMimeType.APPLICATION_AVRO; assertThat(mime.getIdentifier()).as("AVRO identifier").isZero(); - ByteBuf encoded = StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime, 0); + ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime, 0); assertThat(toHeaderBits(encoded)) .startsWith("1") .isEqualTo("10000000"); - String decoded = StreamMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); + String decoded = CompositeMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); assertThat(decoded).isEqualTo(mime.toString()); } @@ -43,13 +43,13 @@ void knownMimeHeaderZero_avro() { void knownMimeHeader127_compositeMetadata() { WellKnownMimeType mime = WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA; assertThat(mime.getIdentifier()).as("COMPOSITE METADATA identifier").isEqualTo((byte) 127); - ByteBuf encoded = StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime, 0); + ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime, 0); assertThat(toHeaderBits(encoded)) .startsWith("1") .isEqualTo("11111111"); - String decoded = StreamMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); + String decoded = CompositeMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); assertThat(decoded).isEqualTo(mime.toString()); } @@ -57,13 +57,13 @@ void knownMimeHeader127_compositeMetadata() { @Test void customMimeHeaderLengthOne() { String mimeString ="w"; - ByteBuf encoded = StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); assertThat(toHeaderBits(encoded)) .startsWith("0") .isEqualTo("00000000"); - String decoded = StreamMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); + String decoded = CompositeMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); assertThat(decoded).isEqualTo(mimeString); } @@ -75,13 +75,13 @@ void customMimeHeaderLength127() { builder.append('a'); } String longString = builder.toString(); - ByteBuf encoded = StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, longString, 0); + ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, longString, 0); assertThat(toHeaderBits(encoded)) .startsWith("0") .isEqualTo("01111110"); - String decoded = StreamMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); + String decoded = CompositeMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); assertThat(decoded).isEqualTo(longString); } @@ -93,13 +93,13 @@ void customMimeHeaderLength128() { builder.append('a'); } String longString = builder.toString(); - ByteBuf encoded = StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, longString, 0); + ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, longString, 0); assertThat(toHeaderBits(encoded)) .startsWith("0") .isEqualTo("01111111"); - String decoded = StreamMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); + String decoded = CompositeMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); assertThat(decoded).isEqualTo(longString); } @@ -112,7 +112,7 @@ void customMimeHeaderLength129_encodingFails() { } assertThatIllegalArgumentException() - .isThrownBy(() -> StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, builder.toString(), 0)) + .isThrownBy(() -> CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, builder.toString(), 0)) .withMessage("custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); } @@ -121,49 +121,49 @@ void customMimeHeaderNonAscii_encodingFails() { String mimeNotAscii = "mime/typé"; assertThatIllegalArgumentException() - .isThrownBy(() -> StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeNotAscii, 0)) + .isThrownBy(() -> CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeNotAscii, 0)) .withMessage("custom mime type must be US_ASCII characters only"); } @Test void customMimeHeaderLength0_encodingFails() { assertThatIllegalArgumentException() - .isThrownBy(() -> StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "", 0)) + .isThrownBy(() -> CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "", 0)) .withMessage("custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); } @Test void decodeMetadataLengthFromUntouchedWithKnownMime() { - ByteBuf encoded = StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, WellKnownMimeType.APPLICATION_GZIP, 12); + ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, WellKnownMimeType.APPLICATION_GZIP, 12); - assertThat(StreamMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)) + assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)) .withFailMessage("should not correctly decode if not at correct reader index") .isNotEqualTo(12); } @Test void decodeMetadataLengthFromMimeDecodedWithKnownMime() { - ByteBuf encoded = StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, WellKnownMimeType.APPLICATION_GZIP, 12); - StreamMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); + ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, WellKnownMimeType.APPLICATION_GZIP, 12); + CompositeMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); - assertThat(StreamMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)).isEqualTo(12); + assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)).isEqualTo(12); } @Test void decodeMetadataLengthFromUntouchedWithCustomMime() { - ByteBuf encoded = StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo/bar", 12); + ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo/bar", 12); - assertThat(StreamMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)) + assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)) .withFailMessage("should not correctly decode if not at correct reader index") .isNotEqualTo(12); } @Test void decodeMetadataLengthFromMimeDecodedWithCustomMime() { - ByteBuf encoded = StreamMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo/bar", 12); - StreamMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); + ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo/bar", 12); + CompositeMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); - assertThat(StreamMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)).isEqualTo(12); + assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)).isEqualTo(12); } @Test @@ -185,8 +185,8 @@ void compositeMetadata() { int metadataLength2 = metadata2.readableBytes(); CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - StreamMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); - StreamMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); + CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); + CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); System.out.println(ByteBufUtil.prettyHexDump(compositeMetadata)); compositeMetadata.readByte(); //ignore the "know mime + ID" byte for now @@ -243,27 +243,42 @@ void decodeCompositeMetadata() { metadata2.writeChar('W'); CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - StreamMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); - StreamMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); + CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); + CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); - Map decoded = StreamMetadataFlyweight.decodeToMap(compositeMetadata, false); + Object[] decoded = CompositeMetadataFlyweight.decodeNext(compositeMetadata, false); + assertThat(decoded).as("first decode").hasSize(2); - assertThat(decoded) - .as("decoded keys") - .containsOnlyKeys(WellKnownMimeType.APPLICATION_PDF.getMime(), "application/custom"); + assertThat(decoded[0]) + .as("first mime") + .isInstanceOf(String.class) + .isEqualTo(WellKnownMimeType.APPLICATION_PDF.getMime()); - ByteBuf decoded1 = decoded.get(WellKnownMimeType.APPLICATION_PDF.getMime()); - ByteBuf decoded2 = decoded.get("application/custom"); - - assertThat(decoded1.toString(CharsetUtil.UTF_8)) - .as("metadata1 decoded") + assertThat((ByteBuf) decoded[1]) + .as("first content") + .isEqualByComparingTo(metadata1) + .extracting(o -> o.toString(CharsetUtil.UTF_8)) .isEqualTo("abcdefghijkl"); - System.out.println(ByteBufUtil.hexDump(decoded2)); - assertThat(decoded2) - .as("metadata2 decoded") + decoded = CompositeMetadataFlyweight.decodeNext(compositeMetadata, false); + + assertThat(decoded).as("second decode").hasSize(2); + + assertThat(decoded[0]) + .as("second mime") + .isInstanceOf(String.class) + .isEqualTo("application/custom"); + + assertThat(decoded[1]).isInstanceOf(ByteBuf.class); + ByteBuf secondBuffer = (ByteBuf) decoded[1]; + System.out.println(ByteBufUtil.hexDump(secondBuffer)); + + assertThat(secondBuffer) + .as("second content") .isEqualByComparingTo(metadata2); + + assertThat(CompositeMetadataFlyweight.decodeNext(compositeMetadata, false)).isEmpty(); } @Test @@ -283,16 +298,22 @@ void decodeCompositeMetadataRetainSlices() { metadata2.writeChar('W'); CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - StreamMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); - StreamMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); - - Map decoded = StreamMetadataFlyweight.decodeToMap(compositeMetadata, true); - - assertThat(decoded).allSatisfy((key, buf) -> - assertThat(buf.refCnt()) - .as("metadata buffer retained for " + key) - .isGreaterThan(1) - ); + CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); + CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); + + List bufs = new ArrayList<>(); + Object[] decoded; + do { + decoded = CompositeMetadataFlyweight.decodeNext(compositeMetadata, true); + if (decoded.length == 2) { + bufs.add((ByteBuf) decoded[1]); + } + } while(decoded.length > 0); + + assertThat(bufs) + .as("metadata buffers retained") + .allSatisfy(buf -> assertThat(buf.refCnt()) + .isGreaterThan(1)); } @Test @@ -312,16 +333,22 @@ void decodeCompositeMetadataNoRetainSlices() { metadata2.writeChar('W'); CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - StreamMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); - StreamMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); - - Map decoded = StreamMetadataFlyweight.decodeToMap(compositeMetadata, false); - - assertThat(decoded).allSatisfy((key, buf) -> - assertThat(buf.refCnt()) - .as("metadata buffer not retained for " + key) - .isOne() - ); + CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); + CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); + + List bufs = new ArrayList<>(); + Object[] decoded; + do { + decoded = CompositeMetadataFlyweight.decodeNext(compositeMetadata, false); + if (decoded.length == 2) { + bufs.add((ByteBuf) decoded[1]); + } + } while(decoded.length > 0); + + assertThat(bufs) + .as("metadata buffers not retained") + .allSatisfy(buf -> assertThat(buf.refCnt()) + .isOne()); } } \ No newline at end of file From 65ba6264c82c90b34b31541ff674caf816171b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 16 May 2019 17:37:22 +0200 Subject: [PATCH 05/25] Add CompositeMetadata interface with factory methods to decode/encode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The API is intended as a readonly way of listing the composite metadata mime-buffer pairs, as well as accessing metadata pairs by mime type or index, including the case where a mime type is associated with several metadata entries (`List getAll(String)`)... Signed-off-by: Simon Baslé --- .../rsocket/metadata/CompositeMetadata.java | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java new file mode 100644 index 000000000..8b6bb6a4c --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java @@ -0,0 +1,230 @@ +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A composite metadata, made of one or more {@link CompositeMetadata.Entry}. Each entry gives access to the + * mime type it uses for metadata encoding, but it is permitted that several entries for the same mime type exist. + * Getting an entry by mime type only returns the first matching entry in such a case, so the {@link #getAll(String)} + * alternative is recommended. Both this method and the {@link #getAll()} method return a read-only view of the + * composite. + */ +public interface CompositeMetadata { + + /** + * An entry in a {@link CompositeMetadata}, which exposes the {@link #getMimeType() mime type} and {@link ByteBuf} + * {@link #getMetadata() content} of the metadata entry. + */ + interface Entry { + + /** + * @return the mime type for this entry + */ + String getMimeType(); + + /** + * @return the metadata content of this entry + */ + ByteBuf getMetadata(); + } + + + /** + * Decode a {@link ByteBuf} into a {@link CompositeMetadata}. This is only possible on frame types used to initiate + * interactions, if the SETUP metadata mime type was {@link WellKnownMimeType#MESSAGE_RSOCKET_COMPOSITE_METADATA}. + *

+ * Each entry {@link ByteBuf} is a {@link ByteBuf#readRetainedSlice(int) retained slice} of the original buffer. + * + * @param buffer the buffer to decode + * @return the decoded {@link CompositeMetadata} + */ + static CompositeMetadata decode(ByteBuf buffer) { + List entries = new ArrayList<>(); + while (buffer.isReadable()) { + Object[] entry = CompositeMetadataFlyweight.decodeNext(buffer, true); + String mime = (String) entry[0]; + ByteBuf buf = (ByteBuf) entry[1]; + entries.add(new DefaultEntry(mime, buf)); + } + return new DefaultCompositeMetadata(entries); + } + + /** + * Encode the next part of a composite metadata into a pre-existing {@link CompositeByteBuf} (which can be empty if + * the first part is being encoded). + *

+ * This method moves the composite buffer's {@link ByteBuf#writerIndex()}, and internally allocates one buffer for + * the metadata header using the provided {@link ByteBufAllocator}. The mime type is compressed using the + * {@link WellKnownMimeType}'s identifier. + * + * @param allocator the {@link ByteBufAllocator} to use to encode the metadata headers (mime type id, metadata length) + * @param compositeMetadataBuffer the composite buffer to which this metadata piece is added + * @param mimeType the {@link WellKnownMimeType} to use + * @param contentBuffer the metadata content + */ + static void encode(ByteBufAllocator allocator, CompositeByteBuf compositeMetadataBuffer, WellKnownMimeType mimeType, ByteBuf contentBuffer) { + CompositeMetadataFlyweight.addMetadata(compositeMetadataBuffer, allocator, mimeType, contentBuffer); + } + + /** + * Encode the next part of a composite metadata into a pre-existing {@link CompositeByteBuf} (which can be empty if + * the first part is being encoded). + *

+ * This method moves the composite buffer's {@link ByteBuf#writerIndex()}, and internally allocates one buffer for + * the metadata header using the provided {@link ByteBufAllocator}. The mime type is NOT compressed, even if it + * matches a {@link WellKnownMimeType}, see {@link #encode(ByteBufAllocator, CompositeByteBuf, String, ByteBuf)}. + * + * @param allocator the {@link ByteBufAllocator} to use to encode the metadata headers (mime type length, mime type, metadata length) + * @param compositeMetadataBuffer the composite buffer to which this metadata piece is added + * @param customMimeType the custom mime type to use (always encoded as length + string) + * @param contentBuffer the metadata content + * @see #encode(ByteBufAllocator, CompositeByteBuf, String, ByteBuf) + */ + static void encodeWithoutMimeCompression(ByteBufAllocator allocator, CompositeByteBuf compositeMetadataBuffer, String customMimeType, ByteBuf contentBuffer) { + CompositeMetadataFlyweight.addMetadata(compositeMetadataBuffer, allocator, customMimeType, contentBuffer); + } + + /** + * Encode the next part of a composite metadata into a pre-existing {@link CompositeByteBuf} (which can be empty if + * the first part is being encoded), with attempted mime type compression. + *

+ * This method moves the composite buffer's {@link ByteBuf#writerIndex()}, and internally allocates one buffer for + * the metadata header using the provided {@link ByteBufAllocator}. Compression of the mime type is attempted, by + * first trying to find a matching {@link WellKnownMimeType} and using its identifier instead of a length + type + * encoding. Use {@link #encode(ByteBufAllocator, CompositeByteBuf, WellKnownMimeType, ByteBuf)} directly if you + * already know the {@link WellKnownMimeType} value to use. + * + * @param allocator the {@link ByteBufAllocator} to use to encode the metadata headers: (mime type length+mime type) + * OR mime type id, metadata length + * @param compositeMetadataBuffer the composite buffer to which this metadata piece is added + * @param customOrKnownMimeType the custom mime type to use (only encoded as length+string if no {@link WellKnownMimeType} can be found) + * @param contentBuffer the metadata content + */ + static void encode(ByteBufAllocator allocator, CompositeByteBuf compositeMetadataBuffer, String customOrKnownMimeType, ByteBuf contentBuffer) { + WellKnownMimeType knownType; + try { + knownType = WellKnownMimeType.fromMimeType(customOrKnownMimeType); + } + catch (IllegalArgumentException iae) { + CompositeMetadataFlyweight.addMetadata(compositeMetadataBuffer, allocator, customOrKnownMimeType, contentBuffer); + return; + } + CompositeMetadataFlyweight.addMetadata(compositeMetadataBuffer, allocator, knownType, contentBuffer); + } + + /** + * Get the first {@link CompositeMetadata.Entry} that matches the given mime type, or null if no such + * entry exist. + * + * @param mimeTypeKey the mime type to look up + * @return the metadata entry + */ + Entry get(String mimeTypeKey); + + /** + * Get the {@link CompositeMetadata.Entry} at the given 0-based index. This is equivalent to + * {@link #getAll()}{@link List#get(int) .get(index)}. + * + * @param index the index to look up + * @return the metadata entry + * @throws IndexOutOfBoundsException if the index is negative or greater than or equal to {@link #size()} + */ + Entry get(int index); + + /** + * Get all entries that match the given mime type. An empty list is returned if no such entry exists. + * Entries are presented in an unmodifiable {@link List} in the order they appeared in on the wire. + * + * @param mimeTypeKey the mime type to look up + * @return an unmodifiable {@link List} of matching entries in the composite + */ + List getAll(String mimeTypeKey); + + /** + * Get all entries in the composite. Entries are presented in an unmodifiable {@link List} in the + * order they appeared in on the wire. + * + * @return an unmodifiable {@link List} of all the entries in the composite + */ + List getAll(); + + /** + * Get the number of entries in this composite. + * + * @return the size of the metadata composite + */ + int size(); + + final class DefaultCompositeMetadata implements CompositeMetadata { + + List entries; + + private DefaultCompositeMetadata(List entries) { + this.entries = Collections.unmodifiableList(entries); + } + + @Override + public Entry get(String mimeTypeKey) { + for (Entry entry : entries) { + if (mimeTypeKey.equals(entry.getMimeType())) { + return entry; + } + } + return null; + } + + @Override + public Entry get(int index) { + return entries.get(index); + } + + @Override + public List getAll(String mimeTypeKey) { + List forMimeType = new ArrayList<>(); + for (Entry entry : entries) { + if (mimeTypeKey.equals(entry.getMimeType())) { + forMimeType.add(entry); + } + } + return forMimeType; + } + + @Override + public List getAll() { + return this.entries; //initially wrapped in Collections.unmodifiableList, so safe to return + } + + @Override + public int size() { + return entries.size(); + } + } + + final class DefaultEntry implements Entry { + + private final String mimeType; + private final ByteBuf content; + + DefaultEntry(String mimeType, ByteBuf content) { + this.mimeType = mimeType; + this.content = content; + } + + @Override + public String getMimeType() { + return this.mimeType; + } + + @Override + public ByteBuf getMetadata() { + return this.content; + } + } + +} From 45b22033c5a5e1254db57f675b0fee19aa442755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 16 May 2019 17:38:56 +0200 Subject: [PATCH 06/25] Add first test for CompositeMetadata (decode smoke test) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Simon Baslé --- .../metadata/CompositeMetadataTest.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java new file mode 100644 index 000000000..33558cb1c --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java @@ -0,0 +1,63 @@ +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.netty.util.CharsetUtil; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class CompositeMetadataTest { + + @Test + void decodeCompositeMetadata() { + //metadata 1: + WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; + ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); + metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); + + //metadata 2: + String mimeType2 = "application/custom"; + ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); + metadata2.writeChar('E'); + metadata2.writeChar('∑'); + metadata2.writeChar('é'); + metadata2.writeBoolean(true); + metadata2.writeChar('W'); + + CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); + CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); + + CompositeMetadata metadata = CompositeMetadata.decode(compositeMetadata); + + assertThat(metadata.size()).as("size").isEqualTo(2); + + assertThat(metadata.get(0)) + .satisfies(e -> assertThat(e.getMimeType()) + .as("metadata1 mime") + .isEqualTo(WellKnownMimeType.APPLICATION_PDF.getMime()) + ) + .satisfies(e -> assertThat(e.getMetadata().toString(CharsetUtil.UTF_8)) + .as("metadata1 decoded") + .isEqualTo("abcdefghijkl") + ); + + System.out.println(ByteBufUtil.hexDump(metadata.get(1).getMetadata())); + + assertThat(metadata.get(1)) + .satisfies(e -> assertThat(e.getMimeType()) + .as("metadata2 mime") + .isEqualTo(mimeType2) + ) + .satisfies(e -> assertThat(e.getMetadata()) + .as("metadata2 buffer") + .isEqualByComparingTo(metadata2) + ); + } + + //TODO unit tests for get(String), get(int), getAll(String) and getAll() => read-only nature, out of bounds, etc... + +} \ No newline at end of file From f2302da755cb7b8ce320f640ddd89b2c2256ce17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 20 May 2019 15:38:44 +0200 Subject: [PATCH 07/25] Expose incremental decode method that decodes a single metadata entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Simon Baslé --- .../rsocket/metadata/CompositeMetadata.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java index 8b6bb6a4c..e013aac4b 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java @@ -47,14 +47,30 @@ interface Entry { static CompositeMetadata decode(ByteBuf buffer) { List entries = new ArrayList<>(); while (buffer.isReadable()) { - Object[] entry = CompositeMetadataFlyweight.decodeNext(buffer, true); - String mime = (String) entry[0]; - ByteBuf buf = (ByteBuf) entry[1]; - entries.add(new DefaultEntry(mime, buf)); + entries.add(decodeIncrementally(buffer, true)); } return new DefaultCompositeMetadata(entries); } + /** + * Incrementally decode the next metadata entry from a {@link ByteBuf} into an {@link Entry}. + * This is only possible on frame types used to initiate + * interactions, if the SETUP metadata mime type was {@link WellKnownMimeType#MESSAGE_RSOCKET_COMPOSITE_METADATA}. + *

+ * Each entry {@link ByteBuf} is a {@link ByteBuf#readSlice(int) slice} of the original buffer that can also be + * {@link ByteBuf#readRetainedSlice(int) retained} if needed. + * + * @param buffer the buffer to decode + * @param retainMetadataSlices should each slide be retained when read from the original buffer? + * @return the decoded {@link Entry} + */ + static Entry decodeIncrementally(ByteBuf buffer, boolean retainMetadataSlices) { + Object[] entry = CompositeMetadataFlyweight.decodeNext(buffer, retainMetadataSlices); + String mime = (String) entry[0]; + ByteBuf buf = (ByteBuf) entry[1]; + return new DefaultEntry(mime, buf); + } + /** * Encode the next part of a composite metadata into a pre-existing {@link CompositeByteBuf} (which can be empty if * the first part is being encoded). From e8613169a6427a94166c5e83f9b5fb84c23d940d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 23 May 2019 15:53:21 +0200 Subject: [PATCH 08/25] WellKnownMimeType parsing now avoids exceptions (except null arguments) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead has 2 special enums. int parsing allows detection of potentially legit type that is still unknown to the current implementation, which could help making it more future-proof. Signed-off-by: Simon Baslé --- .../rsocket/metadata/WellKnownMimeType.java | 37 ++++++++++++------- .../metadata/WellKnownMimeTypeTest.java | 35 ++++++++++-------- 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java index 59a2adccc..fe685aa5f 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java @@ -3,9 +3,12 @@ /** * Enumeration of Well Known Mime Types, as defined in the eponymous extension. Such mime types * are used in composite metadata (which can include routing and/or tracing metadata). + * Per specification, identifiers are between 0 and 127 (inclusive). */ public enum WellKnownMimeType { + UNPARSEABLE_MIME_TYPE("UNPARSEABLE_MIME_TYPE_DO_NOT_USE", (byte) -2), + UNKNOWN_RESERVED_MIME_TYPE("UNKNOWN_YET_RESERVED_DO_NOT_USE", (byte) -1), APPLICATION_AVRO("application/avro", (byte)0), APPLICATION_CBOR("application/cbor", (byte)1), APPLICATION_GRAPHQL("application/graphql", (byte)2), @@ -53,7 +56,6 @@ public enum WellKnownMimeType { private final byte identifier; WellKnownMimeType(String str, byte identifier) { - if (identifier < 0) throw new IllegalArgumentException("identifier must be between 0 and 127, inclusive"); this.str = str; this.identifier = identifier; } @@ -81,39 +83,48 @@ public String toString() { } /** - * Find the {@link WellKnownMimeType} for the given {@link String} representation. If the representation if - * {@code null} or doesn't match a {@link WellKnownMimeType}, an {@link IllegalArgumentException} is thrown. + * Find the {@link WellKnownMimeType} for the given {@link String} representation. + * If the representation is {@code null} or doesn't match a {@link WellKnownMimeType}, the + * {@link #UNPARSEABLE_MIME_TYPE} is returned. + * * @param mimeType the looked up mime type - * @return the {@link WellKnownMimeType} - * @throws IllegalArgumentException if the requested type is unknown or null + * @return the matching {@link WellKnownMimeType}, or {@link #UNPARSEABLE_MIME_TYPE} if none matches */ public static WellKnownMimeType fromMimeType(String mimeType) { if (mimeType == null) throw new IllegalArgumentException("type must be non-null"); + + //force UNPARSEABLE if by chance UNKNOWN_RESERVED_MIME_TYPE's text has been used + if (mimeType.equals(UNKNOWN_RESERVED_MIME_TYPE.str)) { + return UNPARSEABLE_MIME_TYPE; + } + for (WellKnownMimeType value : values()) { if (mimeType.equals(value.str)) { return value; } } - throw new IllegalArgumentException("not a WellKnownMimeType: " + mimeType); + return UNPARSEABLE_MIME_TYPE; } /** * Find the {@link WellKnownMimeType} for the given ID (as an int). Valid IDs are defined to be integers between 0 - * and 127, inclusive. However some IDs in that are still only reserved and don't have a type associated yet: this - * method throws an {@link IllegalStateException} when passing such a ID. + * and 127, inclusive. IDs outside of this range will produce the {@link #UNPARSEABLE_MIME_TYPE}. + * Additionally, some IDs in that range are still only reserved and don't have a type associated yet: this + * method returns the {@link #UNKNOWN_RESERVED_MIME_TYPE} when passing such a ID, which lets call sites potentially + * detect this and keep the original representation when transmitting the associated metadata buffer. * * @param id the looked up id - * @return the {@link WellKnownMimeType} - * @throws IllegalArgumentException if the requested id is out of the specification's range - * @throws IllegalStateException if the requested id is one that is merely reserved + * @return the {@link WellKnownMimeType}, or {@link #UNKNOWN_RESERVED_MIME_TYPE} if the id is out of the + * specification's range, or {@link #UNKNOWN_RESERVED_MIME_TYPE} if the id is one that is merely reserved but + * unknown to this implementation. */ public static WellKnownMimeType fromId(int id) { if (id < 0 || id > 127) { - throw new IllegalArgumentException("WellKnownMimeType IDs are between 0 and 127, inclusive"); + return UNPARSEABLE_MIME_TYPE; } for (WellKnownMimeType value : values()) { if (value.getIdentifier() == id) return value; } - throw new IllegalStateException(id + " is between 0 and 127 yet no WellKnownMimeType found"); + return UNKNOWN_RESERVED_MIME_TYPE; } } diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java index ded385011..885018913 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java @@ -4,14 +4,15 @@ import static org.assertj.core.api.Assertions.*; -/** - * @author Simon Baslé - */ class WellKnownMimeTypeTest { @Test void fromIdMatchFromMimeType() { for (WellKnownMimeType mimeType : WellKnownMimeType.values()) { + if (mimeType == WellKnownMimeType.UNPARSEABLE_MIME_TYPE + || mimeType == WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE) { + continue; + } assertThat(WellKnownMimeType.fromMimeType(mimeType.toString())) .as("mimeType string for " + mimeType.name()) .isSameAs(mimeType); @@ -24,30 +25,34 @@ void fromIdMatchFromMimeType() { @Test void fromIdNegative() { - assertThatIllegalArgumentException().isThrownBy(() -> - WellKnownMimeType.fromId(-1)) - .withMessage("WellKnownMimeType IDs are between 0 and 127, inclusive"); + assertThat(WellKnownMimeType.fromId(-1)) + .isSameAs(WellKnownMimeType.fromId(-2)) + .isSameAs(WellKnownMimeType.fromId(-12)) + .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); } @Test void fromIdGreaterThan127() { - assertThatIllegalArgumentException().isThrownBy(() -> - WellKnownMimeType.fromId(128)) - .withMessage("WellKnownMimeType IDs are between 0 and 127, inclusive"); + assertThat(WellKnownMimeType.fromId(128)) + .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); } @Test void fromIdReserved() { - assertThatIllegalStateException().isThrownBy(() -> - WellKnownMimeType.fromId(120)) - .withMessage("120 is between 0 and 127 yet no WellKnownMimeType found"); + assertThat(WellKnownMimeType.fromId(120)) + .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); } @Test void fromMimeTypeUnknown() { - assertThatIllegalArgumentException().isThrownBy(() -> - WellKnownMimeType.fromMimeType("foo/bar")) - .withMessage("not a WellKnownMimeType: foo/bar"); + assertThat(WellKnownMimeType.fromMimeType("foo/bar")) + .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); + } + + @Test + void fromMimeTypeUnkwnowReservedStillReturnsUnparseable() { + assertThat(WellKnownMimeType.fromMimeType(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE.getMime())) + .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); } } \ No newline at end of file From 54cfe650c85f5125998a01c78b06472ee94a4431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Fri, 24 May 2019 19:32:09 +0200 Subject: [PATCH 09/25] rework encoding / decoding API around Entry, accommodate 3 mime case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Encoding becomes a mirror of decoding, purely based on Entry. Accommodate 3 flavors of mime type decoding: - compressed well known with id matching an enum - uncompressed custom string - compressed but known as reserved for future use (future proofness) The later can be decoded into a special Entry that will allow re encoding it and transmitting it to other nodes as it was. Signed-off-by: Simon Baslé --- .../rsocket/metadata/CompositeMetadata.java | 184 +++++++++++----- .../metadata/CompositeMetadataFlyweight.java | 93 +++++--- .../CompositeMetadataFlyweightTest.java | 138 +++++++++--- .../metadata/CompositeMetadataTest.java | 205 +++++++++++++++++- 4 files changed, 498 insertions(+), 122 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java index e013aac4b..2b7ac3be0 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java @@ -23,6 +23,22 @@ public interface CompositeMetadata { */ interface Entry { + /** + * A passthrough entry is one for which the {@link #getMimeType()} could not be decoded. + * This is usually because it was compressed on the wire, but using an id that is still just "reserved for + * future use" in this implementation. + *

+ * Still, another actor on the network might be able to interpret such an entry, which should thus be + * re-encoded as it was when forwarding the frame. + *

+ * The {@link #getMetadata()} exposes the raw content buffer of the entry (as any other entry). + * + * @return true if this entry should be ignored but passed through as is during re-encoding + */ + default boolean isPassthrough() { + return false; + } + /** * @return the mime type for this entry */ @@ -66,72 +82,62 @@ static CompositeMetadata decode(ByteBuf buffer) { */ static Entry decodeIncrementally(ByteBuf buffer, boolean retainMetadataSlices) { Object[] entry = CompositeMetadataFlyweight.decodeNext(buffer, retainMetadataSlices); - String mime = (String) entry[0]; + Object mime = entry[0]; ByteBuf buf = (ByteBuf) entry[1]; - return new DefaultEntry(mime, buf); + if (mime instanceof WellKnownMimeType) { + return new CompressedTypeEntry((WellKnownMimeType) mime, buf); + } + if (mime instanceof Byte) { + return new UnknownCompressedTypeEntry((Byte) mime, buf); + } + return new CustomTypeEntry((String) mime, buf); } /** - * Encode the next part of a composite metadata into a pre-existing {@link CompositeByteBuf} (which can be empty if - * the first part is being encoded). + * Encode a {@link CompositeMetadata} into a new {@link CompositeByteBuf}. *

- * This method moves the composite buffer's {@link ByteBuf#writerIndex()}, and internally allocates one buffer for - * the metadata header using the provided {@link ByteBufAllocator}. The mime type is compressed using the - * {@link WellKnownMimeType}'s identifier. + * This method moves the buffer's {@link ByteBuf#writerIndex()}. + * It uses the existing content {@link ByteBuf} of each {@link Entry}, but allocates a new buffer for each metadata + * header using the provided {@link ByteBufAllocator}. * - * @param allocator the {@link ByteBufAllocator} to use to encode the metadata headers (mime type id, metadata length) - * @param compositeMetadataBuffer the composite buffer to which this metadata piece is added - * @param mimeType the {@link WellKnownMimeType} to use - * @param contentBuffer the metadata content + * @param allocator the {@link ByteBufAllocator} to use when a new buffer is needed + * @param metadata the {@link CompositeMetadata} to encode + * @return a {@link CompositeByteBuf} that represents the {@link CompositeMetadata} */ - static void encode(ByteBufAllocator allocator, CompositeByteBuf compositeMetadataBuffer, WellKnownMimeType mimeType, ByteBuf contentBuffer) { - CompositeMetadataFlyweight.addMetadata(compositeMetadataBuffer, allocator, mimeType, contentBuffer); + static CompositeByteBuf encode(ByteBufAllocator allocator, CompositeMetadata metadata) { + CompositeByteBuf compositeMetadataBuffer = allocator.compositeBuffer(); + for (Entry entry : metadata.getAll()) { + encodeIncrementally(allocator, compositeMetadataBuffer, entry); + } + return compositeMetadataBuffer; } /** - * Encode the next part of a composite metadata into a pre-existing {@link CompositeByteBuf} (which can be empty if - * the first part is being encoded). + * Incrementally encode a {@link CompositeMetadata} by encoding a provided {@link Entry} into a pre-existing + * {@link CompositeByteBuf} (which can be empty if it is the first entry that is being encoded). *

* This method moves the composite buffer's {@link ByteBuf#writerIndex()}, and internally allocates one buffer for - * the metadata header using the provided {@link ByteBufAllocator}. The mime type is NOT compressed, even if it - * matches a {@link WellKnownMimeType}, see {@link #encode(ByteBufAllocator, CompositeByteBuf, String, ByteBuf)}. - * - * @param allocator the {@link ByteBufAllocator} to use to encode the metadata headers (mime type length, mime type, metadata length) - * @param compositeMetadataBuffer the composite buffer to which this metadata piece is added - * @param customMimeType the custom mime type to use (always encoded as length + string) - * @param contentBuffer the metadata content - * @see #encode(ByteBufAllocator, CompositeByteBuf, String, ByteBuf) - */ - static void encodeWithoutMimeCompression(ByteBufAllocator allocator, CompositeByteBuf compositeMetadataBuffer, String customMimeType, ByteBuf contentBuffer) { - CompositeMetadataFlyweight.addMetadata(compositeMetadataBuffer, allocator, customMimeType, contentBuffer); - } - - /** - * Encode the next part of a composite metadata into a pre-existing {@link CompositeByteBuf} (which can be empty if - * the first part is being encoded), with attempted mime type compression. + * the metadata header using the provided {@link ByteBufAllocator}. *

- * This method moves the composite buffer's {@link ByteBuf#writerIndex()}, and internally allocates one buffer for - * the metadata header using the provided {@link ByteBufAllocator}. Compression of the mime type is attempted, by - * first trying to find a matching {@link WellKnownMimeType} and using its identifier instead of a length + type - * encoding. Use {@link #encode(ByteBufAllocator, CompositeByteBuf, WellKnownMimeType, ByteBuf)} directly if you - * already know the {@link WellKnownMimeType} value to use. + * If the mime type is either a {@link WellKnownMimeType} or a {@link WellKnownMimeType#UNKNOWN_RESERVED_MIME_TYPE}, + * it is compressed using the identifier. Otherwise, the {@link String} length + mime type are encoded. * - * @param allocator the {@link ByteBufAllocator} to use to encode the metadata headers: (mime type length+mime type) - * OR mime type id, metadata length + * @param allocator the {@link ByteBufAllocator} to use to encode the metadata headers (mime type id/length+string, metadata length) * @param compositeMetadataBuffer the composite buffer to which this metadata piece is added - * @param customOrKnownMimeType the custom mime type to use (only encoded as length+string if no {@link WellKnownMimeType} can be found) - * @param contentBuffer the metadata content + * @param metadataEntry the {@link Entry} to encode */ - static void encode(ByteBufAllocator allocator, CompositeByteBuf compositeMetadataBuffer, String customOrKnownMimeType, ByteBuf contentBuffer) { - WellKnownMimeType knownType; - try { - knownType = WellKnownMimeType.fromMimeType(customOrKnownMimeType); + static void encodeIncrementally(ByteBufAllocator allocator, CompositeByteBuf compositeMetadataBuffer, Entry metadataEntry) { + if (metadataEntry instanceof UnknownCompressedTypeEntry) { + byte id = ((UnknownCompressedTypeEntry) metadataEntry).getUnknownReservedId(); + CompositeMetadataFlyweight.addMetadata(compositeMetadataBuffer, allocator, id, metadataEntry.getMetadata()); + } + else if (metadataEntry instanceof CompressedTypeEntry) { + WellKnownMimeType mimeType = ((CompressedTypeEntry) metadataEntry).mimeType; + CompositeMetadataFlyweight.addMetadata(compositeMetadataBuffer, allocator, mimeType, metadataEntry.getMetadata()); } - catch (IllegalArgumentException iae) { - CompositeMetadataFlyweight.addMetadata(compositeMetadataBuffer, allocator, customOrKnownMimeType, contentBuffer); - return; + else { + CompositeMetadataFlyweight.addMetadata(compositeMetadataBuffer, allocator, metadataEntry.getMimeType(), metadataEntry.getMetadata()); } - CompositeMetadataFlyweight.addMetadata(compositeMetadataBuffer, allocator, knownType, contentBuffer); } /** @@ -162,6 +168,14 @@ static void encode(ByteBufAllocator allocator, CompositeByteBuf compositeMetadat */ List getAll(String mimeTypeKey); + /** + * Get all entries in the composite, to the exclusion of entries which mime type cannot be parsed, + * but were marked as {@link Entry#isPassthrough()} during decoding. + * + * @return an unmodifiable {@link List} of all the (parseable) entries in the composite + */ + List getAllParseable(); + /** * Get all entries in the composite. Entries are presented in an unmodifiable {@link List} in the * order they appeared in on the wire. @@ -181,7 +195,7 @@ final class DefaultCompositeMetadata implements CompositeMetadata { List entries; - private DefaultCompositeMetadata(List entries) { + DefaultCompositeMetadata(List entries) { this.entries = Collections.unmodifiableList(entries); } @@ -208,7 +222,18 @@ public List getAll(String mimeTypeKey) { forMimeType.add(entry); } } - return forMimeType; + return Collections.unmodifiableList(forMimeType); + } + + @Override + public List getAllParseable() { + List notPassthrough = new ArrayList<>(); + for (Entry entry : entries) { + if (!entry.isPassthrough()) { + notPassthrough.add(entry); + } + } + return Collections.unmodifiableList(notPassthrough); } @Override @@ -222,12 +247,12 @@ public int size() { } } - final class DefaultEntry implements Entry { + final class CustomTypeEntry implements Entry { private final String mimeType; private final ByteBuf content; - DefaultEntry(String mimeType, ByteBuf content) { + CustomTypeEntry(String mimeType, ByteBuf content) { this.mimeType = mimeType; this.content = content; } @@ -243,4 +268,59 @@ public ByteBuf getMetadata() { } } + final class CompressedTypeEntry implements Entry { + + private final WellKnownMimeType mimeType; + private final ByteBuf content; + + CompressedTypeEntry(WellKnownMimeType mimeType, ByteBuf content) { + this.mimeType = mimeType; + this.content = content; + } + + @Override + public String getMimeType() { + return this.mimeType.getMime(); + } + + @Override + public ByteBuf getMetadata() { + return this.content; + } + } + + final class UnknownCompressedTypeEntry implements Entry { + + private final byte identifier; + private final ByteBuf content; + + UnknownCompressedTypeEntry(byte identifier, ByteBuf content) { + this.identifier = identifier; + this.content = content; + } + + @Override + public boolean isPassthrough() { + return true; + } + + @Override + public String getMimeType() { + return WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE.getMime(); + } + + /** + * Return the compressed identifier that was used in the original decoded metadata, but couldn't be + * decoded because this implementation only knows this as "reserved for future use". The original + */ + public byte getUnknownReservedId() { + return this.identifier; + } + + @Override + public ByteBuf getMetadata() { + return this.content; + } + } + } diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java index 0bb039ff0..64e37d961 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java @@ -10,8 +10,8 @@ class CompositeMetadataFlyweight { - private static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 - private static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 + static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 + static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 private CompositeMetadataFlyweight() {} @@ -19,25 +19,28 @@ private CompositeMetadataFlyweight() {} * Decode the next mime type information from a composite metadata buffer which {@link ByteBuf#readerIndex()} is * at the start of the next metadata section. *

- * Mime type is returned as a {@link String} containing only US_ASCII characters, and the index is moved past the - * mime section, to the starting byte of the sub-metadata's length. + * By order of preference the mime type is returned as a {@link WellKnownMimeType} (if encoded as such and recognizable), + * a {@code byte} (if encoded as a {@link WellKnownMimeType} id that is valid BUT unrecognized) or a {@link String} + * containing only US_ASCII characters. The index is moved past the mime section, to the starting byte of the + * sub-metadata's length. * * @param buffer the metadata or composite metadata to read mime information from. - * @return the next metadata mime type as {@link String}. the buffer {@link ByteBuf#readerIndex()} is moved. + * @return the next metadata mime type as {@link String}, a {@link WellKnownMimeType} or a {@code byte}. the buffer {@link ByteBuf#readerIndex()} is moved. */ - static String decodeMimeFromMetadataHeader(ByteBuf buffer) { + static Object decode3WaysMimeFromMetadataHeader(ByteBuf buffer) { byte source = buffer.readByte(); if ((source & STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK) { //M flag set int id = source & STREAM_METADATA_LENGTH_MASK; WellKnownMimeType mime = WellKnownMimeType.fromId(id); - if (mime != null) { - return mime.toString(); + if (mime == WellKnownMimeType.UNPARSEABLE_MIME_TYPE) { + //should not be possible with the mask + throw new IllegalStateException("Mime compression flag detected, but invalid mime id " + id); } - else { - throw new IllegalStateException(Integer.toBinaryString(source) + " is not an expected binary representation"); + if (mime == WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE) { + return (byte) id; } - + return mime; } //M flag unset, remaining 7 bits are the length of the mime int mimeLength = Byte.toUnsignedInt(source) + 1; @@ -61,6 +64,30 @@ static int decodeMetadataLengthFromMetadataHeader(ByteBuf buffer) { return buffer.readUnsignedMedium(); } + /** + * Decode the next composite metadata piece from a composite metadata buffer into an {@link Object} array which + * holds two elements: the mime type (as either a {@code byte}, a {@link String} or a {@link WellKnownMimeType}) and + * the {@link ByteBuf} metadata value. + * The array is empty if the composite metadata buffer has been entirely decoded, but generally this method shouldn't + * be called if the buffer's {@link ByteBuf#isReadable()} method returns {@code false}. + * + * @param compositeMetadata the {@link ByteBuf} that contains information for one or more metadata mime-value pairs, + * with its reader index set at the start of next metadata piece (or end of the buffer if + * fully decoded) + * @param retainMetadataSlices should metadata value {@link ByteBuf} be {@link ByteBuf#retain() retained} when decoded? + * @return the decoded piece of composite metadata, or an empty Object array if no more metadata is in the composite + */ + static Object[] decodeNext(ByteBuf compositeMetadata, boolean retainMetadataSlices) { + if (compositeMetadata.isReadable()) { + Object mime = decode3WaysMimeFromMetadataHeader(compositeMetadata); + int length = decodeMetadataLengthFromMetadataHeader(compositeMetadata); + ByteBuf metadata = retainMetadataSlices ? compositeMetadata.readRetainedSlice(length) : compositeMetadata.readSlice(length); + + return new Object[] {mime, metadata}; + } + return new Object[0]; + } + /** * Encode a {@link WellKnownMimeType well known mime type} and a metadata value length into a newly allocated * {@link ByteBuf}. @@ -69,13 +96,15 @@ static int decodeMetadataLengthFromMetadataHeader(ByteBuf buffer) { * 3 additional bytes. * * @param allocator the {@link ByteBufAllocator} to use to create the buffer. - * @param mimeType a {@link WellKnownMimeType} to encode. + * @param mimeType a byte identifier of a {@link WellKnownMimeType} to encode. * @param metadataLength the metadata length to append to the buffer as an unsigned 24 bits integer. * @return the encoded mime and metadata length information */ - static ByteBuf encodeMetadataHeader(ByteBufAllocator allocator, WellKnownMimeType mimeType, int metadataLength) { + static ByteBuf encodeMetadataHeader(ByteBufAllocator allocator, byte mimeType, int metadataLength) { + byte id = (byte) (mimeType & 0xFF); + ByteBuf buffer = allocator.buffer(4, 4) - .writeByte(mimeType.getIdentifier() | STREAM_METADATA_KNOWN_MASK); + .writeByte(mimeType | STREAM_METADATA_KNOWN_MASK); NumberUtils.encodeUnsignedMedium(buffer, metadataLength); @@ -103,9 +132,10 @@ static ByteBuf encodeMetadataHeader(ByteBufAllocator allocator, String customMim if (ml < 1 || ml > 128) { throw new IllegalArgumentException("custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); } + ml--; ByteBuf mimeLength = allocator.buffer(1,1); - mimeLength.writeByte(ml - 1); + mimeLength.writeByte((byte) ml); ByteBuf metadataLengthBuffer = allocator.buffer(3, 3); NumberUtils.encodeUnsignedMedium(metadataLengthBuffer, metadataLength); @@ -136,34 +166,27 @@ static void addMetadata(CompositeByteBuf compositeMetaData, ByteBufAllocator all * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. * @param knownMimeType the {@link WellKnownMimeType} to encode. * @param metadata the metadata value to encode. - * @see #encodeMetadataHeader(ByteBufAllocator, WellKnownMimeType, int) + * @see #encodeMetadataHeader(ByteBufAllocator, byte, int) */ static void addMetadata(CompositeByteBuf compositeMetaData, ByteBufAllocator allocator, WellKnownMimeType knownMimeType, ByteBuf metadata) { compositeMetaData.addComponents(true, - encodeMetadataHeader(allocator, knownMimeType, metadata.readableBytes()), + encodeMetadataHeader(allocator, knownMimeType.getIdentifier(), metadata.readableBytes()), metadata); } /** - * Decode the next composite metadata piece from a composite metadata buffer into an {@link Object} array which - * holds two elements: the {@link String} mime types and the {@link ByteBuf} metadata value. - * The array is empty if the composite metadata buffer has been entirely decoded, but generally this method shouldn't - * be called if the buffer's {@link ByteBuf#isReadable()} method returns {@code false}. + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf buffer}. * - * @param compositeMetadata the {@link ByteBuf} that contains information for one or more metadata mime-value pairs, - * with its reader index set at the start of next metadata piece (or end of the buffer if - * fully decoded) - * @param retainMetadataSlices should metadata value {@link ByteBuf} be {@link ByteBuf#retain() retained} when decoded? - * @return the decoded piece of composite metadata, or an empty Object array if no more metadata is in the composite + * @param compositeMetaData the buffer that will hold all composite metadata information. + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param unknownCompressedMimeType the id of the {@link WellKnownMimeType#UNKNOWN_RESERVED_MIME_TYPE} to encode. + * @param metadata the metadata value to encode. + * @see #encodeMetadataHeader(ByteBufAllocator, byte, int) */ - static Object[] decodeNext(ByteBuf compositeMetadata, boolean retainMetadataSlices) { - if (compositeMetadata.isReadable()) { - String mime = decodeMimeFromMetadataHeader(compositeMetadata); - int length = decodeMetadataLengthFromMetadataHeader(compositeMetadata); - ByteBuf metadata = retainMetadataSlices ? compositeMetadata.readRetainedSlice(length) : compositeMetadata.readSlice(length); - - return new Object[] {mime, metadata}; - } - return new Object[0]; + static void addMetadata(CompositeByteBuf compositeMetaData, ByteBufAllocator allocator, byte unknownCompressedMimeType, ByteBuf metadata) { + compositeMetaData.addComponents(true, + encodeMetadataHeader(allocator, unknownCompressedMimeType, metadata.readableBytes()), + metadata); } + } diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java index 42495ac89..de003a55f 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java @@ -10,48 +10,77 @@ import java.util.ArrayList; import java.util.List; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.*; class CompositeMetadataFlyweightTest { static String toHeaderBits(ByteBuf encoded) { encoded.markReaderIndex(); byte headerByte = encoded.readByte(); - String byteAsString = String.format("%8s", Integer.toBinaryString(headerByte & 0xFF)).replace(' ', '0'); + String byteAsString = byteToBitsString(headerByte); encoded.resetReaderIndex(); return byteAsString; } + + static String byteToBitsString(byte b) { + return String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0'); + } // ==== @Test void knownMimeHeaderZero_avro() { WellKnownMimeType mime = WellKnownMimeType.APPLICATION_AVRO; - assertThat(mime.getIdentifier()).as("AVRO identifier").isZero(); - ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime, 0); + assertThat(mime.getIdentifier()) + .as("smoke test AVRO unsigned 7 bits representation") + .isEqualTo((byte) 0) + .isEqualTo((byte) 0b00000000); + ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); assertThat(toHeaderBits(encoded)) .startsWith("1") - .isEqualTo("10000000"); + .isEqualTo("10000000") + .isEqualTo(byteToBitsString(mime.getIdentifier()).replaceFirst("0", "1")); - String decoded = CompositeMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); - - assertThat(decoded).isEqualTo(mime.toString()); + assertThat(CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded)) + .isInstanceOf(WellKnownMimeType.class) + .isSameAs(mime); } @Test void knownMimeHeader127_compositeMetadata() { WellKnownMimeType mime = WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA; - assertThat(mime.getIdentifier()).as("COMPOSITE METADATA identifier").isEqualTo((byte) 127); + assertThat(mime.getIdentifier()) + .as("smoke test COMPOSITE unsigned 7 bits representation") + .isEqualTo((byte) 127) + .isEqualTo((byte) 0b01111111); + ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); + + assertThat(toHeaderBits(encoded)) + .startsWith("1") + .isEqualTo("11111111") + .isEqualTo(byteToBitsString(mime.getIdentifier()).replaceFirst("0", "1")); + + assertThat(CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded)) + .isInstanceOf(WellKnownMimeType.class) + .isSameAs(mime); + } + + @Test + void knownMimeHeader120_reserved() { + byte mime = (byte) 120; ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime, 0); + assertThat(mime).as("smoke test RESERVED_120 unsigned 7 bits representation") + .isEqualTo((byte) 0b01111000); + assertThat(toHeaderBits(encoded)) .startsWith("1") - .isEqualTo("11111111"); + .isEqualTo("11111000"); - String decoded = CompositeMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); - assertThat(decoded).isEqualTo(mime.toString()); + assertThat(CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded)) + .isInstanceOf(Byte.class) + .isSameAs(mime); } @Test @@ -63,9 +92,23 @@ void customMimeHeaderLengthOne() { .startsWith("0") .isEqualTo("00000000"); - String decoded = CompositeMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); + assertThat(CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded)) + .isInstanceOf(String.class) + .isEqualTo(mimeString); + } + + @Test + void customMimeHeaderLengthTwo() { + String mimeString ="ww"; + ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + + assertThat(toHeaderBits(encoded)) + .startsWith("0") + .isEqualTo("00000001"); - assertThat(decoded).isEqualTo(mimeString); + assertThat(CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded)) + .isInstanceOf(String.class) + .isEqualTo(mimeString); } @Test @@ -81,9 +124,9 @@ void customMimeHeaderLength127() { .startsWith("0") .isEqualTo("01111110"); - String decoded = CompositeMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); - - assertThat(decoded).isEqualTo(longString); + assertThat(CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded)) + .isInstanceOf(String.class) + .isEqualTo(longString); } @Test @@ -99,9 +142,9 @@ void customMimeHeaderLength128() { .startsWith("0") .isEqualTo("01111111"); - String decoded = CompositeMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); - - assertThat(decoded).isEqualTo(longString); + assertThat(CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded)) + .isInstanceOf(String.class) + .isEqualTo(longString); } @Test @@ -134,7 +177,7 @@ void customMimeHeaderLength0_encodingFails() { @Test void decodeMetadataLengthFromUntouchedWithKnownMime() { - ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, WellKnownMimeType.APPLICATION_GZIP, 12); + ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, WellKnownMimeType.APPLICATION_GZIP.getIdentifier(), 12); assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)) .withFailMessage("should not correctly decode if not at correct reader index") @@ -143,8 +186,8 @@ void decodeMetadataLengthFromUntouchedWithKnownMime() { @Test void decodeMetadataLengthFromMimeDecodedWithKnownMime() { - ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, WellKnownMimeType.APPLICATION_GZIP, 12); - CompositeMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); + ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, WellKnownMimeType.APPLICATION_GZIP.getIdentifier(), 12); + CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded); assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)).isEqualTo(12); } @@ -161,11 +204,21 @@ void decodeMetadataLengthFromUntouchedWithCustomMime() { @Test void decodeMetadataLengthFromMimeDecodedWithCustomMime() { ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo/bar", 12); - CompositeMetadataFlyweight.decodeMimeFromMetadataHeader(encoded); + CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded); assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)).isEqualTo(12); } + @Test + void decodeMetadataLengthFromTooShortBuffer() { + ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); + buffer.writeShort(12); + + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(buffer)) + .withMessage("the given buffer should contain at least 3 readable bytes after decoding mime type"); + } + @Test void compositeMetadata() { //metadata 1: @@ -228,12 +281,12 @@ void compositeMetadata() { @Test void decodeCompositeMetadata() { - //metadata 1: + //metadata 1: well known WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); - //metadata 2: + //metadata 2: custom String mimeType2 = "application/custom"; ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); metadata2.writeChar('E'); @@ -242,17 +295,26 @@ void decodeCompositeMetadata() { metadata2.writeBoolean(true); metadata2.writeChar('W'); + //metadata 3: reserved but unknown + byte reserved = 120; + assertThat(WellKnownMimeType.fromId(reserved)) + .as("ensure UNKNOWN RESERVED used in test") + .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); + ByteBuf metadata3 = ByteBufAllocator.DEFAULT.buffer(); + metadata3.writeByte(88); + CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); + CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, reserved, metadata3); Object[] decoded = CompositeMetadataFlyweight.decodeNext(compositeMetadata, false); assertThat(decoded).as("first decode").hasSize(2); assertThat(decoded[0]) .as("first mime") - .isInstanceOf(String.class) - .isEqualTo(WellKnownMimeType.APPLICATION_PDF.getMime()); + .isInstanceOf(WellKnownMimeType.class) + .isEqualTo(WellKnownMimeType.APPLICATION_PDF); assertThat((ByteBuf) decoded[1]) .as("first content") @@ -278,6 +340,24 @@ void decodeCompositeMetadata() { .as("second content") .isEqualByComparingTo(metadata2); + + decoded = CompositeMetadataFlyweight.decodeNext(compositeMetadata, false); + + assertThat(decoded).as("third decode").hasSize(2); + + assertThat(decoded[0]) + .as("third mime") + .isInstanceOf(Byte.class) + .isEqualTo(reserved); + + assertThat(decoded[1]).isInstanceOf(ByteBuf.class); + ByteBuf thirdBuffer = (ByteBuf) decoded[1]; + + assertThat(thirdBuffer) + .as("third content") + .matches(buf -> buf.readableBytes() == 1, "1 readable byte") + .matches(buf -> buf.readByte() == 88, "byte content 88"); + assertThat(CompositeMetadataFlyweight.decodeNext(compositeMetadata, false)).isEmpty(); } diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java index 33558cb1c..3889948b9 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java @@ -5,20 +5,29 @@ import io.netty.buffer.ByteBufUtil; import io.netty.buffer.CompositeByteBuf; import io.netty.util.CharsetUtil; +import io.rsocket.metadata.CompositeMetadata.CompressedTypeEntry; +import io.rsocket.metadata.CompositeMetadata.CustomTypeEntry; +import io.rsocket.metadata.CompositeMetadata.Entry; +import io.rsocket.metadata.CompositeMetadata.UnknownCompressedTypeEntry; +import io.rsocket.test.util.ByteBufUtils; import org.junit.jupiter.api.Test; +import java.util.Arrays; + +import static io.netty.util.CharsetUtil.UTF_8; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; class CompositeMetadataTest { @Test void decodeCompositeMetadata() { - //metadata 1: + //metadata 1: well known WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); - metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); + metadata1.writeCharSequence("abcdefghijkl", UTF_8); - //metadata 2: + //metadata 2: custom String mimeType2 = "application/custom"; ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); metadata2.writeChar('E'); @@ -27,20 +36,29 @@ void decodeCompositeMetadata() { metadata2.writeBoolean(true); metadata2.writeChar('W'); + //metadata 3: reserved but unknown + byte reserved = 120; + assertThat(WellKnownMimeType.fromId(reserved)) + .as("ensure UNKNOWN RESERVED used in test") + .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); + ByteBuf metadata3 = ByteBufAllocator.DEFAULT.buffer(); + metadata3.writeByte(88); + CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); + CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, reserved, metadata3); CompositeMetadata metadata = CompositeMetadata.decode(compositeMetadata); - assertThat(metadata.size()).as("size").isEqualTo(2); + assertThat(metadata.size()).as("size").isEqualTo(3); assertThat(metadata.get(0)) .satisfies(e -> assertThat(e.getMimeType()) .as("metadata1 mime") .isEqualTo(WellKnownMimeType.APPLICATION_PDF.getMime()) ) - .satisfies(e -> assertThat(e.getMetadata().toString(CharsetUtil.UTF_8)) + .satisfies(e -> assertThat(e.getMetadata().toString(UTF_8)) .as("metadata1 decoded") .isEqualTo("abcdefghijkl") ); @@ -56,8 +74,183 @@ void decodeCompositeMetadata() { .as("metadata2 buffer") .isEqualByComparingTo(metadata2) ); + + assertThat(metadata.get(2)) + .matches(Entry::isPassthrough) + .isInstanceOf(UnknownCompressedTypeEntry.class) + .satisfies(e -> assertThat(e.getMimeType()).isEqualTo(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE.getMime())) + .satisfies(e -> assertThat(((UnknownCompressedTypeEntry) e).getUnknownReservedId()) + .isEqualTo(reserved)); + } + + @Test + void encodeIncrementallyWellKnownMetadata() { + WellKnownMimeType type = WellKnownMimeType.fromId(5); + //5 = 0b00000101 + byte expected = (byte) 0b10000101; + + ByteBuf content = ByteBufUtils.getRandomByteBuf(2); + Entry entry = new CompressedTypeEntry(type, content); + + final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeMetadata.encodeIncrementally(ByteBufAllocator.DEFAULT, metadata, entry); + + assertThat(metadata.readByte()) + .as("mime header") + .isEqualTo(expected); + assertThat(metadata.readUnsignedMedium()).as("length header").isEqualTo(2); + assertThat(metadata.readSlice(2)).as("content").isEqualByComparingTo(content); } - //TODO unit tests for get(String), get(int), getAll(String) and getAll() => read-only nature, out of bounds, etc... + @Test + void encodeIncrementallyCustomMetadata() { + // length 3, encoded as length - 1 since 0 is not authorized + byte expected = (byte) 2; + ByteBuf content = ByteBufUtils.getRandomByteBuf(2); + Entry entry = new CustomTypeEntry("foo", content); + + final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeMetadata.encodeIncrementally(ByteBufAllocator.DEFAULT, metadata, entry); + + assertThat(metadata.readByte()) + .as("mime header") + .isEqualTo(expected); + assertThat(metadata.readCharSequence(3, CharsetUtil.US_ASCII).toString()) + .isEqualTo("foo"); + assertThat(metadata.readUnsignedMedium()).as("length header").isEqualTo(2); + assertThat(metadata.readSlice(2)).as("content").isEqualByComparingTo(content); + } + + @Test + void encodeIncrementallyPassthroughMetadata() { + //120 = 0b01111000 + byte expected = (byte) 0b11111000; + + ByteBuf content = ByteBufUtils.getRandomByteBuf(2); + UnknownCompressedTypeEntry entry = new UnknownCompressedTypeEntry((byte) 120, content); + + final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeMetadata.encodeIncrementally(ByteBufAllocator.DEFAULT, metadata, entry); + + assertThat(metadata.readByte()) + .as("mime header") + .isEqualTo(expected); + assertThat(metadata.readUnsignedMedium()).as("length header").isEqualTo(2); + assertThat(metadata.readSlice(2)).as("content").isEqualByComparingTo(content); + } + + @Test + void encodeMetadata() { + final Entry entry1 = new CustomTypeEntry("foo", + ByteBufUtils.getRandomByteBuf(1)); + + WellKnownMimeType mime2 = WellKnownMimeType.fromId(5); + //5 = 0b00000101 + byte expected2 = (byte) 0b10000101; + final Entry entry2 = new CompressedTypeEntry(mime2, + ByteBufUtils.getRandomByteBuf(2)); + + byte id3 = (byte) 120; + byte expected3 = (byte) 0b11111000; + final Entry entry3 = new UnknownCompressedTypeEntry(id3, + ByteBufUtils.getRandomByteBuf(3)); + + CompositeMetadata compositeMetadata = new CompositeMetadata.DefaultCompositeMetadata(Arrays.asList(entry1, entry2, entry3)); + CompositeByteBuf buf = CompositeMetadata.encode(ByteBufAllocator.DEFAULT, compositeMetadata); + + assertThat(buf.readByte()) + .as("meta1 mime length") + .isEqualTo((byte) 2); + assertThat(buf.readCharSequence(3, CharsetUtil.US_ASCII).toString()) + .as("meta1 mime") + .isEqualTo("foo"); + assertThat(buf.readUnsignedMedium()) + .as("meta1 content length") + .isEqualTo(1); + assertThat(buf.readBytes(1)) + .as("meta1 content") + .isEqualByComparingTo(entry1.getMetadata()); + + assertThat(buf.readByte()) + .as("meta2 id") + .isEqualTo(expected2); + assertThat(buf.readUnsignedMedium()) + .as("meta2 content length") + .isEqualTo(2); + assertThat(buf.readBytes(2)) + .as("meta2 content") + .isEqualByComparingTo(entry2.getMetadata()); + + assertThat(buf.readByte()) + .as("meta3 id") + .isEqualTo(expected3); + assertThat(buf.readUnsignedMedium()) + .as("meta3 content length") + .isEqualTo(3); + assertThat(buf.readBytes(3)) + .as("meta3 content") + .isEqualByComparingTo(entry3.getMetadata()); + } + + @Test + void getForTypeWithTwoMatches() { + ByteBuf noMatch = ByteBufUtils.getRandomByteBuf(2); + ByteBuf match1 = ByteBufUtils.getRandomByteBuf(2); + ByteBuf match2 = ByteBufUtils.getRandomByteBuf(2); + CompositeMetadata metadata = new CompositeMetadata.DefaultCompositeMetadata(Arrays.asList( + new CustomTypeEntry("noMatch", noMatch), + new CustomTypeEntry("match", match1), + new CustomTypeEntry("match", match2) + )); + + assertThat(metadata.get("match").getMetadata()).isSameAs(match1); + assertThat(metadata.getAll("match")) + .flatExtracting(Entry::getMetadata) + .containsExactly(match1, match2); + } + + @Test + void getForTypeWithNoMatch() { + ByteBuf noMatch1 = ByteBufUtils.getRandomByteBuf(2); + ByteBuf noMatch2 = ByteBufUtils.getRandomByteBuf(2); + CompositeMetadata metadata = new CompositeMetadata.DefaultCompositeMetadata(Arrays.asList( + new CustomTypeEntry("noMatch1", noMatch1), + new CustomTypeEntry("noMatch2", noMatch2) + )); + + assertThat(metadata.get("match")).isNull(); + assertThat(metadata.getAll("match")) + .isEmpty(); + } + + @Test + void getAllForTypeIsUnmodifiable() { + ByteBuf match1 = ByteBufUtils.getRandomByteBuf(2); + ByteBuf match2 = ByteBufUtils.getRandomByteBuf(2); + CompositeMetadata metadata = new CompositeMetadata.DefaultCompositeMetadata(Arrays.asList( + new CustomTypeEntry("match1", match1), + new CustomTypeEntry("match2", match2) + )); + + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> metadata.getAll("match").clear()); + } + + @Test + void getAllParseableVsGetAll() { + final Entry entry1 = new CustomTypeEntry("foo", ByteBufUtils.getRandomByteBuf(2)); + final Entry entry2 = new CompressedTypeEntry(WellKnownMimeType.APPLICATION_GZIP, ByteBufUtils.getRandomByteBuf(2)); + final Entry entry3 = new UnknownCompressedTypeEntry((byte) 120, ByteBufUtils.getRandomByteBuf(2)); + + CompositeMetadata metadata = new CompositeMetadata.DefaultCompositeMetadata( + Arrays.asList(entry1, entry2, entry3)); + + assertThat(metadata.getAll()) + .as("getAll()") + .containsExactly(entry1, entry2, entry3); + assertThat(metadata.getAllParseable()) + .as("getAllParseable()") + .containsExactly(entry1, entry2); + } } \ No newline at end of file From cea0a875a2cd19290c332a414bd55ab4c5905fff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Fri, 24 May 2019 19:37:53 +0200 Subject: [PATCH 10/25] No need for a CompositeMetadata interface, single concrete implem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Simon Baslé --- .../rsocket/metadata/CompositeMetadata.java | 125 ++++++++---------- .../metadata/CompositeMetadataTest.java | 10 +- 2 files changed, 59 insertions(+), 76 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java index 2b7ac3be0..7baafccde 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java @@ -15,13 +15,13 @@ * alternative is recommended. Both this method and the {@link #getAll()} method return a read-only view of the * composite. */ -public interface CompositeMetadata { +public final class CompositeMetadata { /** * An entry in a {@link CompositeMetadata}, which exposes the {@link #getMimeType() mime type} and {@link ByteBuf} * {@link #getMetadata() content} of the metadata entry. */ - interface Entry { + public interface Entry { /** * A passthrough entry is one for which the {@link #getMimeType()} could not be decoded. @@ -60,12 +60,12 @@ default boolean isPassthrough() { * @param buffer the buffer to decode * @return the decoded {@link CompositeMetadata} */ - static CompositeMetadata decode(ByteBuf buffer) { + public static CompositeMetadata decode(ByteBuf buffer) { List entries = new ArrayList<>(); while (buffer.isReadable()) { entries.add(decodeIncrementally(buffer, true)); } - return new DefaultCompositeMetadata(entries); + return new CompositeMetadata(entries); } /** @@ -80,7 +80,7 @@ static CompositeMetadata decode(ByteBuf buffer) { * @param retainMetadataSlices should each slide be retained when read from the original buffer? * @return the decoded {@link Entry} */ - static Entry decodeIncrementally(ByteBuf buffer, boolean retainMetadataSlices) { + public static Entry decodeIncrementally(ByteBuf buffer, boolean retainMetadataSlices) { Object[] entry = CompositeMetadataFlyweight.decodeNext(buffer, retainMetadataSlices); Object mime = entry[0]; ByteBuf buf = (ByteBuf) entry[1]; @@ -104,7 +104,7 @@ static Entry decodeIncrementally(ByteBuf buffer, boolean retainMetadataSlices) { * @param metadata the {@link CompositeMetadata} to encode * @return a {@link CompositeByteBuf} that represents the {@link CompositeMetadata} */ - static CompositeByteBuf encode(ByteBufAllocator allocator, CompositeMetadata metadata) { + public static CompositeByteBuf encode(ByteBufAllocator allocator, CompositeMetadata metadata) { CompositeByteBuf compositeMetadataBuffer = allocator.compositeBuffer(); for (Entry entry : metadata.getAll()) { encodeIncrementally(allocator, compositeMetadataBuffer, entry); @@ -126,7 +126,7 @@ static CompositeByteBuf encode(ByteBufAllocator allocator, CompositeMetadata met * @param compositeMetadataBuffer the composite buffer to which this metadata piece is added * @param metadataEntry the {@link Entry} to encode */ - static void encodeIncrementally(ByteBufAllocator allocator, CompositeByteBuf compositeMetadataBuffer, Entry metadataEntry) { + public static void encodeIncrementally(ByteBufAllocator allocator, CompositeByteBuf compositeMetadataBuffer, Entry metadataEntry) { if (metadataEntry instanceof UnknownCompressedTypeEntry) { byte id = ((UnknownCompressedTypeEntry) metadataEntry).getUnknownReservedId(); CompositeMetadataFlyweight.addMetadata(compositeMetadataBuffer, allocator, id, metadataEntry.getMetadata()); @@ -140,6 +140,12 @@ else if (metadataEntry instanceof CompressedTypeEntry) { } } + List entries; + + CompositeMetadata(List entries) { + this.entries = Collections.unmodifiableList(entries); + } + /** * Get the first {@link CompositeMetadata.Entry} that matches the given mime type, or null if no such * entry exist. @@ -147,7 +153,14 @@ else if (metadataEntry instanceof CompressedTypeEntry) { * @param mimeTypeKey the mime type to look up * @return the metadata entry */ - Entry get(String mimeTypeKey); + public Entry get(String mimeTypeKey) { + for (Entry entry : entries) { + if (mimeTypeKey.equals(entry.getMimeType())) { + return entry; + } + } + return null; + } /** * Get the {@link CompositeMetadata.Entry} at the given 0-based index. This is equivalent to @@ -157,7 +170,9 @@ else if (metadataEntry instanceof CompressedTypeEntry) { * @return the metadata entry * @throws IndexOutOfBoundsException if the index is negative or greater than or equal to {@link #size()} */ - Entry get(int index); + public Entry get(int index) { + return entries.get(index); + } /** * Get all entries that match the given mime type. An empty list is returned if no such entry exists. @@ -166,7 +181,15 @@ else if (metadataEntry instanceof CompressedTypeEntry) { * @param mimeTypeKey the mime type to look up * @return an unmodifiable {@link List} of matching entries in the composite */ - List getAll(String mimeTypeKey); + public List getAll(String mimeTypeKey) { + List forMimeType = new ArrayList<>(); + for (Entry entry : entries) { + if (mimeTypeKey.equals(entry.getMimeType())) { + forMimeType.add(entry); + } + } + return Collections.unmodifiableList(forMimeType); + } /** * Get all entries in the composite, to the exclusion of entries which mime type cannot be parsed, @@ -174,7 +197,15 @@ else if (metadataEntry instanceof CompressedTypeEntry) { * * @return an unmodifiable {@link List} of all the (parseable) entries in the composite */ - List getAllParseable(); + public List getAllParseable() { + List notPassthrough = new ArrayList<>(); + for (Entry entry : entries) { + if (!entry.isPassthrough()) { + notPassthrough.add(entry); + } + } + return Collections.unmodifiableList(notPassthrough); + } /** * Get all entries in the composite. Entries are presented in an unmodifiable {@link List} in the @@ -182,72 +213,20 @@ else if (metadataEntry instanceof CompressedTypeEntry) { * * @return an unmodifiable {@link List} of all the entries in the composite */ - List getAll(); + public List getAll() { + return this.entries; //initially wrapped in Collections.unmodifiableList, so safe to return + } /** * Get the number of entries in this composite. * * @return the size of the metadata composite */ - int size(); - - final class DefaultCompositeMetadata implements CompositeMetadata { - - List entries; - - DefaultCompositeMetadata(List entries) { - this.entries = Collections.unmodifiableList(entries); - } - - @Override - public Entry get(String mimeTypeKey) { - for (Entry entry : entries) { - if (mimeTypeKey.equals(entry.getMimeType())) { - return entry; - } - } - return null; - } - - @Override - public Entry get(int index) { - return entries.get(index); - } - - @Override - public List getAll(String mimeTypeKey) { - List forMimeType = new ArrayList<>(); - for (Entry entry : entries) { - if (mimeTypeKey.equals(entry.getMimeType())) { - forMimeType.add(entry); - } - } - return Collections.unmodifiableList(forMimeType); - } - - @Override - public List getAllParseable() { - List notPassthrough = new ArrayList<>(); - for (Entry entry : entries) { - if (!entry.isPassthrough()) { - notPassthrough.add(entry); - } - } - return Collections.unmodifiableList(notPassthrough); - } - - @Override - public List getAll() { - return this.entries; //initially wrapped in Collections.unmodifiableList, so safe to return - } - - @Override - public int size() { - return entries.size(); - } + public int size() { + return entries.size(); } - final class CustomTypeEntry implements Entry { + static final class CustomTypeEntry implements Entry { private final String mimeType; private final ByteBuf content; @@ -268,7 +247,7 @@ public ByteBuf getMetadata() { } } - final class CompressedTypeEntry implements Entry { + static final class CompressedTypeEntry implements Entry { private final WellKnownMimeType mimeType; private final ByteBuf content; @@ -289,7 +268,11 @@ public ByteBuf getMetadata() { } } - final class UnknownCompressedTypeEntry implements Entry { + /** + * A {@link Entry#isPassthrough() pass-through} {@link Entry} that encapsulates a + * compressed-mime metadata that couldn't be recognized. + */ + public static final class UnknownCompressedTypeEntry implements Entry { private final byte identifier; private final ByteBuf content; diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java index 3889948b9..8cc73966f 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java @@ -155,7 +155,7 @@ void encodeMetadata() { final Entry entry3 = new UnknownCompressedTypeEntry(id3, ByteBufUtils.getRandomByteBuf(3)); - CompositeMetadata compositeMetadata = new CompositeMetadata.DefaultCompositeMetadata(Arrays.asList(entry1, entry2, entry3)); + CompositeMetadata compositeMetadata = new CompositeMetadata(Arrays.asList(entry1, entry2, entry3)); CompositeByteBuf buf = CompositeMetadata.encode(ByteBufAllocator.DEFAULT, compositeMetadata); assertThat(buf.readByte()) @@ -197,7 +197,7 @@ void getForTypeWithTwoMatches() { ByteBuf noMatch = ByteBufUtils.getRandomByteBuf(2); ByteBuf match1 = ByteBufUtils.getRandomByteBuf(2); ByteBuf match2 = ByteBufUtils.getRandomByteBuf(2); - CompositeMetadata metadata = new CompositeMetadata.DefaultCompositeMetadata(Arrays.asList( + CompositeMetadata metadata = new CompositeMetadata(Arrays.asList( new CustomTypeEntry("noMatch", noMatch), new CustomTypeEntry("match", match1), new CustomTypeEntry("match", match2) @@ -213,7 +213,7 @@ void getForTypeWithTwoMatches() { void getForTypeWithNoMatch() { ByteBuf noMatch1 = ByteBufUtils.getRandomByteBuf(2); ByteBuf noMatch2 = ByteBufUtils.getRandomByteBuf(2); - CompositeMetadata metadata = new CompositeMetadata.DefaultCompositeMetadata(Arrays.asList( + CompositeMetadata metadata = new CompositeMetadata(Arrays.asList( new CustomTypeEntry("noMatch1", noMatch1), new CustomTypeEntry("noMatch2", noMatch2) )); @@ -227,7 +227,7 @@ void getForTypeWithNoMatch() { void getAllForTypeIsUnmodifiable() { ByteBuf match1 = ByteBufUtils.getRandomByteBuf(2); ByteBuf match2 = ByteBufUtils.getRandomByteBuf(2); - CompositeMetadata metadata = new CompositeMetadata.DefaultCompositeMetadata(Arrays.asList( + CompositeMetadata metadata = new CompositeMetadata(Arrays.asList( new CustomTypeEntry("match1", match1), new CustomTypeEntry("match2", match2) )); @@ -242,7 +242,7 @@ void getAllParseableVsGetAll() { final Entry entry2 = new CompressedTypeEntry(WellKnownMimeType.APPLICATION_GZIP, ByteBufUtils.getRandomByteBuf(2)); final Entry entry3 = new UnknownCompressedTypeEntry((byte) 120, ByteBufUtils.getRandomByteBuf(2)); - CompositeMetadata metadata = new CompositeMetadata.DefaultCompositeMetadata( + CompositeMetadata metadata = new CompositeMetadata( Arrays.asList(entry1, entry2, entry3)); assertThat(metadata.getAll()) From 436426277b55b170f18dd7783c1de77d610aaa26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Tue, 28 May 2019 18:11:00 +0200 Subject: [PATCH 11/25] Expose the CompositeMetadataFlyweight for low-level, simplify API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - the flyweight deals with low-level encoding/decoding with minimal garbage and only uses exceptions in the encoding path -- encoding a non-ASCII custom mime type OR -- encoding an out of range byte - the CompositeMetadata is an _option_ for a higher level Iterable-based API, but which instantiates new types Signed-off-by: Simon Baslé --- .../rsocket/metadata/CompositeMetadata.java | 59 +- .../metadata/CompositeMetadataFlyweight.java | 166 ++--- .../CompositeMetadataFlyweightTest.java | 593 ++++++++++-------- .../metadata/CompositeMetadataTest.java | 75 ++- 4 files changed, 541 insertions(+), 352 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java index 7baafccde..4c4d611bc 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java @@ -60,10 +60,10 @@ default boolean isPassthrough() { * @param buffer the buffer to decode * @return the decoded {@link CompositeMetadata} */ - public static CompositeMetadata decode(ByteBuf buffer) { + public static CompositeMetadata decodeComposite(ByteBuf buffer) { List entries = new ArrayList<>(); while (buffer.isReadable()) { - entries.add(decodeIncrementally(buffer, true)); + entries.add(decodeEntry(buffer, true)); } return new CompositeMetadata(entries); } @@ -80,17 +80,42 @@ public static CompositeMetadata decode(ByteBuf buffer) { * @param retainMetadataSlices should each slide be retained when read from the original buffer? * @return the decoded {@link Entry} */ - public static Entry decodeIncrementally(ByteBuf buffer, boolean retainMetadataSlices) { - Object[] entry = CompositeMetadataFlyweight.decodeNext(buffer, retainMetadataSlices); - Object mime = entry[0]; - ByteBuf buf = (ByteBuf) entry[1]; - if (mime instanceof WellKnownMimeType) { - return new CompressedTypeEntry((WellKnownMimeType) mime, buf); + public static Entry decodeEntry(ByteBuf buffer, boolean retainMetadataSlices) { + ByteBuf[] entry = CompositeMetadataFlyweight.decodeMimeAndContentBuffers(buffer, retainMetadataSlices); + if (entry == CompositeMetadataFlyweight.METADATA_MALFORMED) { + throw new IllegalArgumentException("composite metadata entry buffer is too short to contain proper entry"); } - if (mime instanceof Byte) { - return new UnknownCompressedTypeEntry((Byte) mime, buf); + if (entry == CompositeMetadataFlyweight.METADATA_BUFFERS_DONE) { + return null; + } + + ByteBuf encodedHeader = entry[0]; + ByteBuf metadataContent = entry[1]; + + + //the flyweight already validated the size of the buffer, + //this is only to distinguish id vs custom type + if (encodedHeader.readableBytes() == 1) { + //id + byte id = CompositeMetadataFlyweight.decodeMimeIdFromMimeBuffer(encodedHeader); + WellKnownMimeType wkn = WellKnownMimeType.fromId(id); + if (wkn == WellKnownMimeType.UNPARSEABLE_MIME_TYPE) { + //should not happen due to flyweight's decodeEntry own guard + throw new IllegalStateException("composite metadata entry parsing failed on compressed mime id " + id); + } + if (wkn == WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE) { + return new UnknownCompressedTypeEntry(id, metadataContent); + } + return new CompressedTypeEntry(wkn, metadataContent); + } + else { + CharSequence customMimeCharSequence = CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(encodedHeader); + if (customMimeCharSequence == null) { + //should not happen due to flyweight's decodeEntry own guard + throw new IllegalArgumentException("composite metadata entry parsing failed on custom type"); + } + return new CustomTypeEntry(customMimeCharSequence.toString(), metadataContent); } - return new CustomTypeEntry((String) mime, buf); } /** @@ -104,10 +129,10 @@ public static Entry decodeIncrementally(ByteBuf buffer, boolean retainMetadataSl * @param metadata the {@link CompositeMetadata} to encode * @return a {@link CompositeByteBuf} that represents the {@link CompositeMetadata} */ - public static CompositeByteBuf encode(ByteBufAllocator allocator, CompositeMetadata metadata) { + public static CompositeByteBuf encodeComposite(ByteBufAllocator allocator, CompositeMetadata metadata) { CompositeByteBuf compositeMetadataBuffer = allocator.compositeBuffer(); for (Entry entry : metadata.getAll()) { - encodeIncrementally(allocator, compositeMetadataBuffer, entry); + encodeEntry(allocator, compositeMetadataBuffer, entry); } return compositeMetadataBuffer; } @@ -126,17 +151,17 @@ public static CompositeByteBuf encode(ByteBufAllocator allocator, CompositeMetad * @param compositeMetadataBuffer the composite buffer to which this metadata piece is added * @param metadataEntry the {@link Entry} to encode */ - public static void encodeIncrementally(ByteBufAllocator allocator, CompositeByteBuf compositeMetadataBuffer, Entry metadataEntry) { + public static void encodeEntry(ByteBufAllocator allocator, CompositeByteBuf compositeMetadataBuffer, Entry metadataEntry) { if (metadataEntry instanceof UnknownCompressedTypeEntry) { byte id = ((UnknownCompressedTypeEntry) metadataEntry).getUnknownReservedId(); - CompositeMetadataFlyweight.addMetadata(compositeMetadataBuffer, allocator, id, metadataEntry.getMetadata()); + CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadataBuffer, allocator, id, metadataEntry.getMetadata()); } else if (metadataEntry instanceof CompressedTypeEntry) { WellKnownMimeType mimeType = ((CompressedTypeEntry) metadataEntry).mimeType; - CompositeMetadataFlyweight.addMetadata(compositeMetadataBuffer, allocator, mimeType, metadataEntry.getMetadata()); + CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadataBuffer, allocator, mimeType, metadataEntry.getMetadata()); } else { - CompositeMetadataFlyweight.addMetadata(compositeMetadataBuffer, allocator, metadataEntry.getMimeType(), metadataEntry.getMetadata()); + CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadataBuffer, allocator, metadataEntry.getMimeType(), metadataEntry.getMetadata()); } } diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java index 64e37d961..a9e729b9b 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java @@ -6,86 +6,110 @@ import io.netty.buffer.CompositeByteBuf; import io.netty.util.CharsetUtil; import io.rsocket.util.NumberUtils; +import reactor.util.annotation.Nullable; -class CompositeMetadataFlyweight { - +public class CompositeMetadataFlyweight { static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 - private CompositeMetadataFlyweight() {} - /** - * Decode the next mime type information from a composite metadata buffer which {@link ByteBuf#readerIndex()} is - * at the start of the next metadata section. - *

- * By order of preference the mime type is returned as a {@link WellKnownMimeType} (if encoded as such and recognizable), - * a {@code byte} (if encoded as a {@link WellKnownMimeType} id that is valid BUT unrecognized) or a {@link String} - * containing only US_ASCII characters. The index is moved past the mime section, to the starting byte of the - * sub-metadata's length. - * - * @param buffer the metadata or composite metadata to read mime information from. - * @return the next metadata mime type as {@link String}, a {@link WellKnownMimeType} or a {@code byte}. the buffer {@link ByteBuf#readerIndex()} is moved. + * Denotes that an attempt at 0-garbage decoding failed because the input buffer + * didn't have enough bytes to represent a complete metadata entry, only part of + * the bytes. + */ + public static final ByteBuf[] METADATA_MALFORMED = new ByteBuf[0]; + /** + * Denotes that an attempt at garbage-free decoding failed because the input buffer + * was completely empty, which generally means that no more entries are present in + * the buffer. */ - static Object decode3WaysMimeFromMetadataHeader(ByteBuf buffer) { - byte source = buffer.readByte(); - if ((source & STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK) { - //M flag set - int id = source & STREAM_METADATA_LENGTH_MASK; - WellKnownMimeType mime = WellKnownMimeType.fromId(id); - if (mime == WellKnownMimeType.UNPARSEABLE_MIME_TYPE) { - //should not be possible with the mask - throw new IllegalStateException("Mime compression flag detected, but invalid mime id " + id); + public static final ByteBuf[] METADATA_BUFFERS_DONE = new ByteBuf[0]; + /** + * Denotes that an attempt at higher level decoding of an entry components failed because + * the input buffer was completely empty, which generally means that no more entries are + * present in the buffer. + */ + static final Object[] METADATA_ENTRIES_DONE = new Object[0]; + + private CompositeMetadataFlyweight() {} + + //TODO document how to go from ByteBuf to id/string mime and distinguish + public static ByteBuf[] decodeMimeAndContentBuffers(ByteBuf compositeMetadata, boolean retainSlices) { + if (compositeMetadata.isReadable()) { + ByteBuf mime; + int ridx = compositeMetadata.readerIndex(); + byte mimeIdOrLength = compositeMetadata.readByte(); + if ((mimeIdOrLength & STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK) { + mime = retainSlices ? + compositeMetadata.retainedSlice(ridx, 1) : + compositeMetadata.slice(ridx, 1); } - if (mime == WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE) { - return (byte) id; + else { + //M flag unset, remaining 7 bits are the length of the mime + int mimeLength = Byte.toUnsignedInt(mimeIdOrLength) + 1; + + if (compositeMetadata.isReadable(mimeLength)) { //need to be able to read an extra mimeLength bytes + //here we need a way for the returned ByteBuf to differentiate between a + //1-byte length mime type and a 1 byte encoded mime id, preferably without + //re-applying the byte mask. The easiest way is to include the initial byte + //and have further decoding ignore the first byte. 1 byte buffer == id, 2+ byte + //buffer == full mime string. + mime = retainSlices ? + // we accommodate that we don't read from current readerIndex, but + //readerIndex - 1 ("0"), for a total slice size of mimeLength + 1 + compositeMetadata.retainedSlice(ridx, mimeLength + 1) : + compositeMetadata.slice(ridx, mimeLength + 1); + //we thus need to skip the bytes we just sliced, but not the flag/length byte + //which was already skipped in initial read + compositeMetadata.skipBytes(mimeLength); + } + else { + return METADATA_MALFORMED; + } + } + + if (compositeMetadata.isReadable(3)) { + //ensures the length medium can be read + final int metadataLength = compositeMetadata.readUnsignedMedium(); + if (compositeMetadata.isReadable(metadataLength)) { + ByteBuf metadata = retainSlices ? + compositeMetadata.readRetainedSlice(metadataLength) : + compositeMetadata.readSlice(metadataLength); + return new ByteBuf[] { mime, metadata }; + } + else { + return METADATA_MALFORMED; + } + } + else { + return METADATA_MALFORMED; } - return mime; } - //M flag unset, remaining 7 bits are the length of the mime - int mimeLength = Byte.toUnsignedInt(source) + 1; - CharSequence mime = buffer.readCharSequence(mimeLength, CharsetUtil.US_ASCII); - return mime.toString(); + return METADATA_BUFFERS_DONE; } - /** - * Decode the current metadata length information from a composite metadata buffer which {@link ByteBuf#readerIndex()} - * is just past the current metadata section's mime information. - *

- * The index is moved past the metadata length section, to the starting byte of the current metadata's value. - * - * @param buffer the metadata or composite metadata to read length information from. - * @return the next metadata length. the buffer {@link ByteBuf#readerIndex()} is moved. - */ - static int decodeMetadataLengthFromMetadataHeader(ByteBuf buffer) { - if (buffer.readableBytes() < 3) { - throw new IllegalStateException("the given buffer should contain at least 3 readable bytes after decoding mime type"); + //TODO document how to check buffer is indeed an id + //TODO document error conditions + public static byte decodeMimeIdFromMimeBuffer(ByteBuf mimeBuffer) { + if (mimeBuffer.readableBytes() != 1) { + return WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier(); } - return buffer.readUnsignedMedium(); + return (byte) (mimeBuffer.readByte() & STREAM_METADATA_LENGTH_MASK); } - /** - * Decode the next composite metadata piece from a composite metadata buffer into an {@link Object} array which - * holds two elements: the mime type (as either a {@code byte}, a {@link String} or a {@link WellKnownMimeType}) and - * the {@link ByteBuf} metadata value. - * The array is empty if the composite metadata buffer has been entirely decoded, but generally this method shouldn't - * be called if the buffer's {@link ByteBuf#isReadable()} method returns {@code false}. - * - * @param compositeMetadata the {@link ByteBuf} that contains information for one or more metadata mime-value pairs, - * with its reader index set at the start of next metadata piece (or end of the buffer if - * fully decoded) - * @param retainMetadataSlices should metadata value {@link ByteBuf} be {@link ByteBuf#retain() retained} when decoded? - * @return the decoded piece of composite metadata, or an empty Object array if no more metadata is in the composite - */ - static Object[] decodeNext(ByteBuf compositeMetadata, boolean retainMetadataSlices) { - if (compositeMetadata.isReadable()) { - Object mime = decode3WaysMimeFromMetadataHeader(compositeMetadata); - int length = decodeMetadataLengthFromMetadataHeader(compositeMetadata); - ByteBuf metadata = retainMetadataSlices ? compositeMetadata.readRetainedSlice(length) : compositeMetadata.readSlice(length); - - return new Object[] {mime, metadata}; + //TODO document error conditions + @Nullable + public static CharSequence decodeMimeTypeFromMimeBuffer(ByteBuf flyweightMimeBuffer) { + if (flyweightMimeBuffer.readableBytes() < 2) { + return null; } - return new Object[0]; + //the encoded length is assumed to be kept at the start of the buffer + //but also assumed to be irrelevant because the rest of the slice length + //actually already matches _decoded_length + flyweightMimeBuffer.skipBytes(1); + int mimeStringLength = flyweightMimeBuffer.readableBytes(); + return flyweightMimeBuffer.readCharSequence(mimeStringLength, CharsetUtil.US_ASCII); } /** @@ -101,8 +125,6 @@ static Object[] decodeNext(ByteBuf compositeMetadata, boolean retainMetadataSlic * @return the encoded mime and metadata length information */ static ByteBuf encodeMetadataHeader(ByteBufAllocator allocator, byte mimeType, int metadataLength) { - byte id = (byte) (mimeType & 0xFF); - ByteBuf buffer = allocator.buffer(4, 4) .writeByte(mimeType | STREAM_METADATA_KNOWN_MASK); @@ -151,9 +173,9 @@ static ByteBuf encodeMetadataHeader(ByteBufAllocator allocator, String customMim * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. * @param customMimeType the custom mime type to encode. * @param metadata the metadata value to encode. - * @see #encodeMetadataHeader(ByteBufAllocator, String, int) */ - static void addMetadata(CompositeByteBuf compositeMetaData, ByteBufAllocator allocator, String customMimeType, ByteBuf metadata) { + //see #encodeMetadataHeader(ByteBufAllocator, String, int) + public static void encodeAndAddMetadata(CompositeByteBuf compositeMetaData, ByteBufAllocator allocator, String customMimeType, ByteBuf metadata) { compositeMetaData.addComponents(true, encodeMetadataHeader(allocator, customMimeType, metadata.readableBytes()), metadata); @@ -166,9 +188,9 @@ static void addMetadata(CompositeByteBuf compositeMetaData, ByteBufAllocator all * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. * @param knownMimeType the {@link WellKnownMimeType} to encode. * @param metadata the metadata value to encode. - * @see #encodeMetadataHeader(ByteBufAllocator, byte, int) */ - static void addMetadata(CompositeByteBuf compositeMetaData, ByteBufAllocator allocator, WellKnownMimeType knownMimeType, ByteBuf metadata) { + // see #encodeMetadataHeader(ByteBufAllocator, byte, int) + public static void encodeAndAddMetadata(CompositeByteBuf compositeMetaData, ByteBufAllocator allocator, WellKnownMimeType knownMimeType, ByteBuf metadata) { compositeMetaData.addComponents(true, encodeMetadataHeader(allocator, knownMimeType.getIdentifier(), metadata.readableBytes()), metadata); @@ -181,9 +203,9 @@ static void addMetadata(CompositeByteBuf compositeMetaData, ByteBufAllocator all * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. * @param unknownCompressedMimeType the id of the {@link WellKnownMimeType#UNKNOWN_RESERVED_MIME_TYPE} to encode. * @param metadata the metadata value to encode. - * @see #encodeMetadataHeader(ByteBufAllocator, byte, int) */ - static void addMetadata(CompositeByteBuf compositeMetaData, ByteBufAllocator allocator, byte unknownCompressedMimeType, ByteBuf metadata) { + // see #encodeMetadataHeader(ByteBufAllocator, byte, int) + static void encodeAndAddMetadata(CompositeByteBuf compositeMetaData, ByteBufAllocator allocator, byte unknownCompressedMimeType, ByteBuf metadata) { compositeMetaData.addComponents(true, encodeMetadataHeader(allocator, unknownCompressedMimeType, metadata.readableBytes()), metadata); diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java index de003a55f..fdd41e33b 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java @@ -2,15 +2,15 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; import io.netty.buffer.CompositeByteBuf; import io.netty.util.CharsetUtil; +import io.rsocket.test.util.ByteBufUtils; +import io.rsocket.util.NumberUtils; import org.junit.jupiter.api.Test; -import java.util.ArrayList; -import java.util.List; - -import static org.assertj.core.api.Assertions.*; +import static io.rsocket.metadata.CompositeMetadataFlyweight.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; class CompositeMetadataFlyweightTest { @@ -41,9 +41,31 @@ void knownMimeHeaderZero_avro() { .isEqualTo("10000000") .isEqualTo(byteToBitsString(mime.getIdentifier()).replaceFirst("0", "1")); - assertThat(CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded)) - .isInstanceOf(WellKnownMimeType.class) - .isSameAs(mime); + final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + assertThat(byteBufs) + .hasSize(2) + .doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()) + .as("metadata header size") + .isOne(); + + assertThat(byteToBitsString(header.readByte())) + .as("header bit representation") + .isEqualTo("10000000"); + + header.resetReaderIndex(); + assertThat(decodeMimeIdFromMimeBuffer(header)) + .as("decoded mime id") + .isEqualTo(mime.getIdentifier()); + + assertThat(content.readableBytes()) + .as("no metadata content") + .isZero(); } @Test @@ -60,9 +82,31 @@ void knownMimeHeader127_compositeMetadata() { .isEqualTo("11111111") .isEqualTo(byteToBitsString(mime.getIdentifier()).replaceFirst("0", "1")); - assertThat(CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded)) - .isInstanceOf(WellKnownMimeType.class) - .isSameAs(mime); + final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + assertThat(byteBufs) + .hasSize(2) + .doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()) + .as("metadata header size") + .isOne(); + + assertThat(byteToBitsString(header.readByte())) + .as("header bit representation") + .isEqualTo("11111111"); + + header.resetReaderIndex(); + assertThat(decodeMimeIdFromMimeBuffer(header)) + .as("decoded mime id") + .isEqualTo(mime.getIdentifier()); + + assertThat(content.readableBytes()) + .as("no metadata content") + .isZero(); } @Test @@ -77,10 +121,31 @@ void knownMimeHeader120_reserved() { .startsWith("1") .isEqualTo("11111000"); + final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + assertThat(byteBufs) + .hasSize(2) + .doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()) + .as("metadata header size") + .isOne(); - assertThat(CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded)) - .isInstanceOf(Byte.class) - .isSameAs(mime); + assertThat(byteToBitsString(header.readByte())) + .as("header bit representation") + .isEqualTo("11111000"); + + header.resetReaderIndex(); + assertThat(decodeMimeIdFromMimeBuffer(header)) + .as("decoded mime id") + .isEqualTo(mime); + + assertThat(content.readableBytes()) + .as("no metadata content") + .isZero(); } @Test @@ -92,9 +157,35 @@ void customMimeHeaderLengthOne() { .startsWith("0") .isEqualTo("00000000"); - assertThat(CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded)) - .isInstanceOf(String.class) - .isEqualTo(mimeString); + final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + assertThat(byteBufs) + .hasSize(2) + .doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()) + .as("metadata header size") + .isGreaterThan(1); + + assertThat((int) header.readByte()) + .as("mime length") + .isEqualTo(1 -1); //encoded as actual length - 1 + + assertThat(header.readCharSequence(1, CharsetUtil.US_ASCII)) + .as("mime string") + .hasToString(mimeString); + + header.resetReaderIndex(); + assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) + .as("decoded mime string") + .hasToString(mimeString); + + assertThat(content.readableBytes()) + .as("no metadata content") + .isZero(); } @Test @@ -106,9 +197,35 @@ void customMimeHeaderLengthTwo() { .startsWith("0") .isEqualTo("00000001"); - assertThat(CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded)) - .isInstanceOf(String.class) - .isEqualTo(mimeString); + final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + assertThat(byteBufs) + .hasSize(2) + .doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()) + .as("metadata header size") + .isGreaterThan(1); + + assertThat((int) header.readByte()) + .as("mime length") + .isEqualTo(2 - 1); //encoded as actual length - 1 + + assertThat(header.readCharSequence(2, CharsetUtil.US_ASCII)) + .as("mime string") + .hasToString(mimeString); + + header.resetReaderIndex(); + assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) + .as("decoded mime string") + .hasToString(mimeString); + + assertThat(content.readableBytes()) + .as("no metadata content") + .isZero(); } @Test @@ -117,16 +234,42 @@ void customMimeHeaderLength127() { for (int i = 0; i < 127; i++) { builder.append('a'); } - String longString = builder.toString(); - ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, longString, 0); + String mimeString = builder.toString(); + ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); assertThat(toHeaderBits(encoded)) .startsWith("0") .isEqualTo("01111110"); - assertThat(CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded)) - .isInstanceOf(String.class) - .isEqualTo(longString); + final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + assertThat(byteBufs) + .hasSize(2) + .doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()) + .as("metadata header size") + .isGreaterThan(1); + + assertThat((int) header.readByte()) + .as("mime length") + .isEqualTo(127 - 1); //encoded as actual length - 1 + + assertThat(header.readCharSequence(127, CharsetUtil.US_ASCII)) + .as("mime string") + .hasToString(mimeString); + + header.resetReaderIndex(); + assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) + .as("decoded mime string") + .hasToString(mimeString); + + assertThat(content.readableBytes()) + .as("no metadata content") + .isZero(); } @Test @@ -135,16 +278,42 @@ void customMimeHeaderLength128() { for (int i = 0; i < 128; i++) { builder.append('a'); } - String longString = builder.toString(); - ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, longString, 0); + String mimeString = builder.toString(); + ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); assertThat(toHeaderBits(encoded)) .startsWith("0") .isEqualTo("01111111"); - assertThat(CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded)) - .isInstanceOf(String.class) - .isEqualTo(longString); + final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + assertThat(byteBufs) + .hasSize(2) + .doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()) + .as("metadata header size") + .isGreaterThan(1); + + assertThat((int) header.readByte()) + .as("mime length") + .isEqualTo(128 - 1); //encoded as actual length - 1 + + assertThat(header.readCharSequence(128, CharsetUtil.US_ASCII)) + .as("mime string") + .hasToString(mimeString); + + header.resetReaderIndex(); + assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) + .as("decoded mime string") + .hasToString(mimeString); + + assertThat(content.readableBytes()) + .as("no metadata content") + .isZero(); } @Test @@ -176,259 +345,187 @@ void customMimeHeaderLength0_encodingFails() { } @Test - void decodeMetadataLengthFromUntouchedWithKnownMime() { - ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, WellKnownMimeType.APPLICATION_GZIP.getIdentifier(), 12); + void decodeEntryTooShortForMimeLength() { + ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + fakeEntry.writeByte(120); - assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)) - .withFailMessage("should not correctly decode if not at correct reader index") - .isNotEqualTo(12); + assertThat(decodeMimeAndContentBuffers(fakeEntry, false)) + .isSameAs(METADATA_MALFORMED); } @Test - void decodeMetadataLengthFromMimeDecodedWithKnownMime() { - ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, WellKnownMimeType.APPLICATION_GZIP.getIdentifier(), 12); - CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded); + void decodeEntryHasNoContentLength() { + ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + fakeEntry.writeByte(0); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); - assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)).isEqualTo(12); + assertThat(decodeMimeAndContentBuffers(fakeEntry, false)) + .isSameAs(METADATA_MALFORMED); } @Test - void decodeMetadataLengthFromUntouchedWithCustomMime() { - ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo/bar", 12); - - assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)) - .withFailMessage("should not correctly decode if not at correct reader index") - .isNotEqualTo(12); + void decodeEntryTooShortForContentLength() { + ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + fakeEntry.writeByte(1); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); + NumberUtils.encodeUnsignedMedium(fakeEntry, 456); + fakeEntry.writeChar('w'); + + assertThat(decodeMimeAndContentBuffers(fakeEntry, false)) + .isSameAs(METADATA_MALFORMED); } @Test - void decodeMetadataLengthFromMimeDecodedWithCustomMime() { - ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo/bar", 12); - CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded); + void decodeEntryAtEndOfBuffer() { + ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); - assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)).isEqualTo(12); + assertThat(decodeMimeAndContentBuffers(fakeEntry, false)) + .isSameAs(METADATA_BUFFERS_DONE); } @Test - void decodeMetadataLengthFromTooShortBuffer() { - ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); - buffer.writeShort(12); + void decodeIdMinusTwoWhenZeroByte() { + ByteBuf fakeIdBuffer = ByteBufAllocator.DEFAULT.buffer(0); - assertThatExceptionOfType(RuntimeException.class) - .isThrownBy(() -> CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(buffer)) - .withMessage("the given buffer should contain at least 3 readable bytes after decoding mime type"); + assertThat(decodeMimeIdFromMimeBuffer(fakeIdBuffer)) + .isEqualTo((WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier())); } @Test - void compositeMetadata() { - //metadata 1: - WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; - ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); - metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); - int metadataLength1 = metadata1.readableBytes(); - - //metadata 2: - String mimeType2 = "application/custom"; - ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); - metadata2.writeChar('E'); - metadata2.writeChar('∑'); - metadata2.writeChar('é'); - metadata2.writeBoolean(true); - metadata2.writeChar('W'); - int metadataLength2 = metadata2.readableBytes(); - - CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); - CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); - System.out.println(ByteBufUtil.prettyHexDump(compositeMetadata)); - - compositeMetadata.readByte(); //ignore the "know mime + ID" byte for now - assertThat(compositeMetadata.readUnsignedMedium()) - .as("metadata1 length") - .isEqualTo(metadataLength1); - assertThat(compositeMetadata.readCharSequence(metadataLength1, CharsetUtil.UTF_8)) - .as("metadata1 value").isEqualTo("abcdefghijkl"); - - int mimeLength = compositeMetadata.readByte() + 1; - - assertThat(compositeMetadata.readCharSequence(mimeLength, CharsetUtil.US_ASCII).toString()) - .as("metadata2 custom mime ") - .isEqualTo(mimeType2); - assertThat(compositeMetadata.readUnsignedMedium()) - .as("metadata2 length") - .isEqualTo(metadataLength2); - assertThat(compositeMetadata.readChar()) - .as("metadata2 value 1/5") - .isEqualTo('E'); - assertThat(compositeMetadata.readChar()) - .as("metadata2 value 2/5") - .isEqualTo('∑'); - - assertThat(compositeMetadata.readChar()) - .as("metadata2 value 3/5") - .isEqualTo('é'); - assertThat(compositeMetadata.readBoolean()) - .as("metadata2 value 4/5") - .isTrue(); - assertThat(compositeMetadata.readChar()) - .as("metadata2 value 5/5") - .isEqualTo('W'); - - assertThat(compositeMetadata.readableBytes()) - .as("reading composite metadata done") - .isZero(); + void decodeIdMinusTwoWhenMoreThanOneByte() { + ByteBuf fakeIdBuffer = ByteBufAllocator.DEFAULT.buffer(2); + fakeIdBuffer.writeInt(200); + + assertThat(decodeMimeIdFromMimeBuffer(fakeIdBuffer)) + .isEqualTo((WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier())); } @Test - void decodeCompositeMetadata() { - //metadata 1: well known - WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; - ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); - metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); - - //metadata 2: custom - String mimeType2 = "application/custom"; - ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); - metadata2.writeChar('E'); - metadata2.writeChar('∑'); - metadata2.writeChar('é'); - metadata2.writeBoolean(true); - metadata2.writeChar('W'); - - //metadata 3: reserved but unknown - byte reserved = 120; - assertThat(WellKnownMimeType.fromId(reserved)) - .as("ensure UNKNOWN RESERVED used in test") - .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); - ByteBuf metadata3 = ByteBufAllocator.DEFAULT.buffer(); - metadata3.writeByte(88); - - CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); - CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); - CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, reserved, metadata3); - - Object[] decoded = CompositeMetadataFlyweight.decodeNext(compositeMetadata, false); - assertThat(decoded).as("first decode").hasSize(2); + void decodeStringNullIfLengthZero() { + ByteBuf fakeTypeBuffer = ByteBufAllocator.DEFAULT.buffer(2); - assertThat(decoded[0]) - .as("first mime") - .isInstanceOf(WellKnownMimeType.class) - .isEqualTo(WellKnownMimeType.APPLICATION_PDF); - - assertThat((ByteBuf) decoded[1]) - .as("first content") - .isEqualByComparingTo(metadata1) - .extracting(o -> o.toString(CharsetUtil.UTF_8)) - .isEqualTo("abcdefghijkl"); - - - decoded = CompositeMetadataFlyweight.decodeNext(compositeMetadata, false); - - assertThat(decoded).as("second decode").hasSize(2); + assertThat(decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)) + .isNull(); + } - assertThat(decoded[0]) - .as("second mime") - .isInstanceOf(String.class) - .isEqualTo("application/custom"); + @Test + void decodeStringNullIfLengthOne() { + ByteBuf fakeTypeBuffer = ByteBufAllocator.DEFAULT.buffer(2); + fakeTypeBuffer.writeByte(1); - assertThat(decoded[1]).isInstanceOf(ByteBuf.class); - ByteBuf secondBuffer = (ByteBuf) decoded[1]; - System.out.println(ByteBufUtil.hexDump(secondBuffer)); + assertThat(decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)) + .isNull(); + } - assertThat(secondBuffer) - .as("second content") - .isEqualByComparingTo(metadata2); + @Test + void decodeTypeSkipsFirstByte() { + ByteBuf fakeTypeBuffer = ByteBufAllocator.DEFAULT.buffer(2); + fakeTypeBuffer.writeByte(128); + fakeTypeBuffer.writeCharSequence("example", CharsetUtil.US_ASCII); + assertThat(decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)) + .hasToString("example"); + } - decoded = CompositeMetadataFlyweight.decodeNext(compositeMetadata, false); + @Test + void encodeMetadataKnownTypeDelegates() { + ByteBuf expected = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, + WellKnownMimeType.APPLICATION_OCTET_STREAM.getIdentifier(), + 2); + + CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataFlyweight.encodeAndAddMetadata( + test, ByteBufAllocator.DEFAULT, + WellKnownMimeType.APPLICATION_OCTET_STREAM, + ByteBufUtils.getRandomByteBuf(2)); + + assertThat((Iterable) test) + .hasSize(2) + .first() + .isEqualTo(expected); + } - assertThat(decoded).as("third decode").hasSize(2); + @Test + void encodeMetadataReservedTypeDelegates() { + ByteBuf expected = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, + (byte) 120, + 2); + + CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataFlyweight.encodeAndAddMetadata( + test, ByteBufAllocator.DEFAULT, + (byte) 120, + ByteBufUtils.getRandomByteBuf(2)); + + assertThat((Iterable) test) + .hasSize(2) + .first() + .isEqualTo(expected); + } - assertThat(decoded[0]) - .as("third mime") - .isInstanceOf(Byte.class) - .isEqualTo(reserved); + @Test + void encodeMetadataCustomTypeDelegates() { + ByteBuf expected = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, + "foo", 2); - assertThat(decoded[1]).isInstanceOf(ByteBuf.class); - ByteBuf thirdBuffer = (ByteBuf) decoded[1]; + CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); - assertThat(thirdBuffer) - .as("third content") - .matches(buf -> buf.readableBytes() == 1, "1 readable byte") - .matches(buf -> buf.readByte() == 88, "byte content 88"); + CompositeMetadataFlyweight.encodeAndAddMetadata( + test, ByteBufAllocator.DEFAULT, + "foo", + ByteBufUtils.getRandomByteBuf(2)); - assertThat(CompositeMetadataFlyweight.decodeNext(compositeMetadata, false)).isEmpty(); + assertThat((Iterable) test) + .hasSize(2) + .first() + .isEqualTo(expected); } - @Test - void decodeCompositeMetadataRetainSlices() { - //metadata 1: - WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; - ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); - metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); - - //metadata 2: - String mimeType2 = "application/custom"; - ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); - metadata2.writeChar('E'); - metadata2.writeChar('∑'); - metadata2.writeChar('é'); - metadata2.writeBoolean(true); - metadata2.writeChar('W'); - - CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); - CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); - - List bufs = new ArrayList<>(); - Object[] decoded; - do { - decoded = CompositeMetadataFlyweight.decodeNext(compositeMetadata, true); - if (decoded.length == 2) { - bufs.add((ByteBuf) decoded[1]); - } - } while(decoded.length > 0); - - assertThat(bufs) - .as("metadata buffers retained") - .allSatisfy(buf -> assertThat(buf.refCnt()) - .isGreaterThan(1)); - } +// @Test +// void decodeMetadataLengthFromUntouchedWithKnownMime() { +// ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, WellKnownMimeType.APPLICATION_GZIP.getIdentifier(), 12); +// +// assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)) +// .withFailMessage("should not correctly decode if not at correct reader index") +// .isNotEqualTo(12); +// } +// +// @Test +// void decodeMetadataLengthFromMimeDecodedWithKnownMime() { +// ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, WellKnownMimeType.APPLICATION_GZIP.getIdentifier(), 12); +// CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded); +// +// assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)).isEqualTo(12); +// } +// +// @Test +// void decodeMetadataLengthFromUntouchedWithCustomMime() { +// ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo/bar", 12); +// +// assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)) +// .withFailMessage("should not correctly decode if not at correct reader index") +// .isNotEqualTo(12); +// } +// +// @Test +// void decodeMetadataLengthFromMimeDecodedWithCustomMime() { +// ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo/bar", 12); +// CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded); +// +// assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)).isEqualTo(12); +// } +// +// @Test +// void decodeMetadataLengthFromTooShortBuffer() { +// ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); +// buffer.writeShort(12); +// +// assertThatExceptionOfType(RuntimeException.class) +// .isThrownBy(() -> CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(buffer)) +// .withMessage("the given buffer should contain at least 3 readable bytes after decoding mime type"); +// } - @Test - void decodeCompositeMetadataNoRetainSlices() { - //metadata 1: - WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; - ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); - metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); - - //metadata 2: - String mimeType2 = "application/custom"; - ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); - metadata2.writeChar('E'); - metadata2.writeChar('∑'); - metadata2.writeChar('é'); - metadata2.writeBoolean(true); - metadata2.writeChar('W'); - - CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); - CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); - - List bufs = new ArrayList<>(); - Object[] decoded; - do { - decoded = CompositeMetadataFlyweight.decodeNext(compositeMetadata, false); - if (decoded.length == 2) { - bufs.add((ByteBuf) decoded[1]); - } - } while(decoded.length > 0); - - assertThat(bufs) - .as("metadata buffers not retained") - .allSatisfy(buf -> assertThat(buf.refCnt()) - .isOne()); - } } \ No newline at end of file diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java index 8cc73966f..81e84c9f8 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java @@ -10,13 +10,13 @@ import io.rsocket.metadata.CompositeMetadata.Entry; import io.rsocket.metadata.CompositeMetadata.UnknownCompressedTypeEntry; import io.rsocket.test.util.ByteBufUtils; +import io.rsocket.util.NumberUtils; import org.junit.jupiter.api.Test; import java.util.Arrays; import static io.netty.util.CharsetUtil.UTF_8; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.*; class CompositeMetadataTest { @@ -45,11 +45,11 @@ void decodeCompositeMetadata() { metadata3.writeByte(88); CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); - CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); - CompositeMetadataFlyweight.addMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, reserved, metadata3); + CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); + CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); + CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, reserved, metadata3); - CompositeMetadata metadata = CompositeMetadata.decode(compositeMetadata); + CompositeMetadata metadata = CompositeMetadata.decodeComposite(compositeMetadata); assertThat(metadata.size()).as("size").isEqualTo(3); @@ -84,7 +84,7 @@ void decodeCompositeMetadata() { } @Test - void encodeIncrementallyWellKnownMetadata() { + void encodeEntryWellKnownMetadata() { WellKnownMimeType type = WellKnownMimeType.fromId(5); //5 = 0b00000101 byte expected = (byte) 0b10000101; @@ -93,7 +93,7 @@ void encodeIncrementallyWellKnownMetadata() { Entry entry = new CompressedTypeEntry(type, content); final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - CompositeMetadata.encodeIncrementally(ByteBufAllocator.DEFAULT, metadata, entry); + CompositeMetadata.encodeEntry(ByteBufAllocator.DEFAULT, metadata, entry); assertThat(metadata.readByte()) .as("mime header") @@ -103,14 +103,14 @@ void encodeIncrementallyWellKnownMetadata() { } @Test - void encodeIncrementallyCustomMetadata() { + void encodeEntryCustomMetadata() { // length 3, encoded as length - 1 since 0 is not authorized byte expected = (byte) 2; ByteBuf content = ByteBufUtils.getRandomByteBuf(2); Entry entry = new CustomTypeEntry("foo", content); final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - CompositeMetadata.encodeIncrementally(ByteBufAllocator.DEFAULT, metadata, entry); + CompositeMetadata.encodeEntry(ByteBufAllocator.DEFAULT, metadata, entry); assertThat(metadata.readByte()) .as("mime header") @@ -122,7 +122,7 @@ void encodeIncrementallyCustomMetadata() { } @Test - void encodeIncrementallyPassthroughMetadata() { + void encodeEntryPassthroughMetadata() { //120 = 0b01111000 byte expected = (byte) 0b11111000; @@ -130,7 +130,7 @@ void encodeIncrementallyPassthroughMetadata() { UnknownCompressedTypeEntry entry = new UnknownCompressedTypeEntry((byte) 120, content); final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - CompositeMetadata.encodeIncrementally(ByteBufAllocator.DEFAULT, metadata, entry); + CompositeMetadata.encodeEntry(ByteBufAllocator.DEFAULT, metadata, entry); assertThat(metadata.readByte()) .as("mime header") @@ -140,7 +140,7 @@ void encodeIncrementallyPassthroughMetadata() { } @Test - void encodeMetadata() { + void encodeCompositeMetadata() { final Entry entry1 = new CustomTypeEntry("foo", ByteBufUtils.getRandomByteBuf(1)); @@ -156,7 +156,7 @@ void encodeMetadata() { ByteBufUtils.getRandomByteBuf(3)); CompositeMetadata compositeMetadata = new CompositeMetadata(Arrays.asList(entry1, entry2, entry3)); - CompositeByteBuf buf = CompositeMetadata.encode(ByteBufAllocator.DEFAULT, compositeMetadata); + CompositeByteBuf buf = CompositeMetadata.encodeComposite(ByteBufAllocator.DEFAULT, compositeMetadata); assertThat(buf.readByte()) .as("meta1 mime length") @@ -203,7 +203,10 @@ void getForTypeWithTwoMatches() { new CustomTypeEntry("match", match2) )); - assertThat(metadata.get("match").getMetadata()).isSameAs(match1); + assertThat(metadata.get("match")) + .isNotNull() + .extracting(Entry::getMetadata) + .isSameAs(match1); assertThat(metadata.getAll("match")) .flatExtracting(Entry::getMetadata) .containsExactly(match1, match2); @@ -253,4 +256,46 @@ void getAllParseableVsGetAll() { .containsExactly(entry1, entry2); } + @Test + void decodeEntryTooShortForMimeLength() { + ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + fakeEntry.writeByte(120); + + assertThatIllegalArgumentException() + .isThrownBy(() -> CompositeMetadata.decodeEntry(fakeEntry, false)) + .withMessage("composite metadata entry buffer is too short to contain proper entry"); + } + + @Test + void decodeEntryHasNoContentLength() { + ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + fakeEntry.writeByte(0); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); + + assertThatIllegalArgumentException() + .isThrownBy(() -> CompositeMetadata.decodeEntry(fakeEntry, false)) + .withMessage("composite metadata entry buffer is too short to contain proper entry"); + } + + @Test + void decodeEntryTooShortForContentLength() { + ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + fakeEntry.writeByte(1); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); + NumberUtils.encodeUnsignedMedium(fakeEntry, 456); + fakeEntry.writeChar('w'); + + assertThatIllegalArgumentException() + .isThrownBy(() -> CompositeMetadata.decodeEntry(fakeEntry, false)) + .withMessage("composite metadata entry buffer is too short to contain proper entry"); + } + + @Test + void decodeEntryOnDoneBufferReturnsNull() { + ByteBuf fakeBuffer = ByteBufUtils.getRandomByteBuf(0); + + assertThat(CompositeMetadata.decodeEntry(fakeBuffer, false)) + .as("empty entry") + .isNull(); + } } \ No newline at end of file From 6a77dd22010ada6e0d61cc500c962463e9836ab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 3 Jun 2019 11:20:52 +0200 Subject: [PATCH 12/25] Remove CompositeMetadata, make Entry child of flyweight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entry is now the only high-level abstraction, will need a way to decode into a List. Signed-off-by: Simon Baslé --- .../rsocket/metadata/CompositeMetadata.java | 334 ------------------ .../metadata/CompositeMetadataFlyweight.java | 239 ++++++++++++- .../CompositeMetadataFlyweightTest.java | 2 +- .../metadata/CompositeMetadataTest.java | 301 ---------------- .../java/io/rsocket/metadata/EntryTest.java | 117 ++++++ 5 files changed, 353 insertions(+), 640 deletions(-) delete mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java delete mode 100644 rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java deleted file mode 100644 index 4c4d611bc..000000000 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java +++ /dev/null @@ -1,334 +0,0 @@ -package io.rsocket.metadata; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.CompositeByteBuf; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * A composite metadata, made of one or more {@link CompositeMetadata.Entry}. Each entry gives access to the - * mime type it uses for metadata encoding, but it is permitted that several entries for the same mime type exist. - * Getting an entry by mime type only returns the first matching entry in such a case, so the {@link #getAll(String)} - * alternative is recommended. Both this method and the {@link #getAll()} method return a read-only view of the - * composite. - */ -public final class CompositeMetadata { - - /** - * An entry in a {@link CompositeMetadata}, which exposes the {@link #getMimeType() mime type} and {@link ByteBuf} - * {@link #getMetadata() content} of the metadata entry. - */ - public interface Entry { - - /** - * A passthrough entry is one for which the {@link #getMimeType()} could not be decoded. - * This is usually because it was compressed on the wire, but using an id that is still just "reserved for - * future use" in this implementation. - *

- * Still, another actor on the network might be able to interpret such an entry, which should thus be - * re-encoded as it was when forwarding the frame. - *

- * The {@link #getMetadata()} exposes the raw content buffer of the entry (as any other entry). - * - * @return true if this entry should be ignored but passed through as is during re-encoding - */ - default boolean isPassthrough() { - return false; - } - - /** - * @return the mime type for this entry - */ - String getMimeType(); - - /** - * @return the metadata content of this entry - */ - ByteBuf getMetadata(); - } - - - /** - * Decode a {@link ByteBuf} into a {@link CompositeMetadata}. This is only possible on frame types used to initiate - * interactions, if the SETUP metadata mime type was {@link WellKnownMimeType#MESSAGE_RSOCKET_COMPOSITE_METADATA}. - *

- * Each entry {@link ByteBuf} is a {@link ByteBuf#readRetainedSlice(int) retained slice} of the original buffer. - * - * @param buffer the buffer to decode - * @return the decoded {@link CompositeMetadata} - */ - public static CompositeMetadata decodeComposite(ByteBuf buffer) { - List entries = new ArrayList<>(); - while (buffer.isReadable()) { - entries.add(decodeEntry(buffer, true)); - } - return new CompositeMetadata(entries); - } - - /** - * Incrementally decode the next metadata entry from a {@link ByteBuf} into an {@link Entry}. - * This is only possible on frame types used to initiate - * interactions, if the SETUP metadata mime type was {@link WellKnownMimeType#MESSAGE_RSOCKET_COMPOSITE_METADATA}. - *

- * Each entry {@link ByteBuf} is a {@link ByteBuf#readSlice(int) slice} of the original buffer that can also be - * {@link ByteBuf#readRetainedSlice(int) retained} if needed. - * - * @param buffer the buffer to decode - * @param retainMetadataSlices should each slide be retained when read from the original buffer? - * @return the decoded {@link Entry} - */ - public static Entry decodeEntry(ByteBuf buffer, boolean retainMetadataSlices) { - ByteBuf[] entry = CompositeMetadataFlyweight.decodeMimeAndContentBuffers(buffer, retainMetadataSlices); - if (entry == CompositeMetadataFlyweight.METADATA_MALFORMED) { - throw new IllegalArgumentException("composite metadata entry buffer is too short to contain proper entry"); - } - if (entry == CompositeMetadataFlyweight.METADATA_BUFFERS_DONE) { - return null; - } - - ByteBuf encodedHeader = entry[0]; - ByteBuf metadataContent = entry[1]; - - - //the flyweight already validated the size of the buffer, - //this is only to distinguish id vs custom type - if (encodedHeader.readableBytes() == 1) { - //id - byte id = CompositeMetadataFlyweight.decodeMimeIdFromMimeBuffer(encodedHeader); - WellKnownMimeType wkn = WellKnownMimeType.fromId(id); - if (wkn == WellKnownMimeType.UNPARSEABLE_MIME_TYPE) { - //should not happen due to flyweight's decodeEntry own guard - throw new IllegalStateException("composite metadata entry parsing failed on compressed mime id " + id); - } - if (wkn == WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE) { - return new UnknownCompressedTypeEntry(id, metadataContent); - } - return new CompressedTypeEntry(wkn, metadataContent); - } - else { - CharSequence customMimeCharSequence = CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(encodedHeader); - if (customMimeCharSequence == null) { - //should not happen due to flyweight's decodeEntry own guard - throw new IllegalArgumentException("composite metadata entry parsing failed on custom type"); - } - return new CustomTypeEntry(customMimeCharSequence.toString(), metadataContent); - } - } - - /** - * Encode a {@link CompositeMetadata} into a new {@link CompositeByteBuf}. - *

- * This method moves the buffer's {@link ByteBuf#writerIndex()}. - * It uses the existing content {@link ByteBuf} of each {@link Entry}, but allocates a new buffer for each metadata - * header using the provided {@link ByteBufAllocator}. - * - * @param allocator the {@link ByteBufAllocator} to use when a new buffer is needed - * @param metadata the {@link CompositeMetadata} to encode - * @return a {@link CompositeByteBuf} that represents the {@link CompositeMetadata} - */ - public static CompositeByteBuf encodeComposite(ByteBufAllocator allocator, CompositeMetadata metadata) { - CompositeByteBuf compositeMetadataBuffer = allocator.compositeBuffer(); - for (Entry entry : metadata.getAll()) { - encodeEntry(allocator, compositeMetadataBuffer, entry); - } - return compositeMetadataBuffer; - } - - /** - * Incrementally encode a {@link CompositeMetadata} by encoding a provided {@link Entry} into a pre-existing - * {@link CompositeByteBuf} (which can be empty if it is the first entry that is being encoded). - *

- * This method moves the composite buffer's {@link ByteBuf#writerIndex()}, and internally allocates one buffer for - * the metadata header using the provided {@link ByteBufAllocator}. - *

- * If the mime type is either a {@link WellKnownMimeType} or a {@link WellKnownMimeType#UNKNOWN_RESERVED_MIME_TYPE}, - * it is compressed using the identifier. Otherwise, the {@link String} length + mime type are encoded. - * - * @param allocator the {@link ByteBufAllocator} to use to encode the metadata headers (mime type id/length+string, metadata length) - * @param compositeMetadataBuffer the composite buffer to which this metadata piece is added - * @param metadataEntry the {@link Entry} to encode - */ - public static void encodeEntry(ByteBufAllocator allocator, CompositeByteBuf compositeMetadataBuffer, Entry metadataEntry) { - if (metadataEntry instanceof UnknownCompressedTypeEntry) { - byte id = ((UnknownCompressedTypeEntry) metadataEntry).getUnknownReservedId(); - CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadataBuffer, allocator, id, metadataEntry.getMetadata()); - } - else if (metadataEntry instanceof CompressedTypeEntry) { - WellKnownMimeType mimeType = ((CompressedTypeEntry) metadataEntry).mimeType; - CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadataBuffer, allocator, mimeType, metadataEntry.getMetadata()); - } - else { - CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadataBuffer, allocator, metadataEntry.getMimeType(), metadataEntry.getMetadata()); - } - } - - List entries; - - CompositeMetadata(List entries) { - this.entries = Collections.unmodifiableList(entries); - } - - /** - * Get the first {@link CompositeMetadata.Entry} that matches the given mime type, or null if no such - * entry exist. - * - * @param mimeTypeKey the mime type to look up - * @return the metadata entry - */ - public Entry get(String mimeTypeKey) { - for (Entry entry : entries) { - if (mimeTypeKey.equals(entry.getMimeType())) { - return entry; - } - } - return null; - } - - /** - * Get the {@link CompositeMetadata.Entry} at the given 0-based index. This is equivalent to - * {@link #getAll()}{@link List#get(int) .get(index)}. - * - * @param index the index to look up - * @return the metadata entry - * @throws IndexOutOfBoundsException if the index is negative or greater than or equal to {@link #size()} - */ - public Entry get(int index) { - return entries.get(index); - } - - /** - * Get all entries that match the given mime type. An empty list is returned if no such entry exists. - * Entries are presented in an unmodifiable {@link List} in the order they appeared in on the wire. - * - * @param mimeTypeKey the mime type to look up - * @return an unmodifiable {@link List} of matching entries in the composite - */ - public List getAll(String mimeTypeKey) { - List forMimeType = new ArrayList<>(); - for (Entry entry : entries) { - if (mimeTypeKey.equals(entry.getMimeType())) { - forMimeType.add(entry); - } - } - return Collections.unmodifiableList(forMimeType); - } - - /** - * Get all entries in the composite, to the exclusion of entries which mime type cannot be parsed, - * but were marked as {@link Entry#isPassthrough()} during decoding. - * - * @return an unmodifiable {@link List} of all the (parseable) entries in the composite - */ - public List getAllParseable() { - List notPassthrough = new ArrayList<>(); - for (Entry entry : entries) { - if (!entry.isPassthrough()) { - notPassthrough.add(entry); - } - } - return Collections.unmodifiableList(notPassthrough); - } - - /** - * Get all entries in the composite. Entries are presented in an unmodifiable {@link List} in the - * order they appeared in on the wire. - * - * @return an unmodifiable {@link List} of all the entries in the composite - */ - public List getAll() { - return this.entries; //initially wrapped in Collections.unmodifiableList, so safe to return - } - - /** - * Get the number of entries in this composite. - * - * @return the size of the metadata composite - */ - public int size() { - return entries.size(); - } - - static final class CustomTypeEntry implements Entry { - - private final String mimeType; - private final ByteBuf content; - - CustomTypeEntry(String mimeType, ByteBuf content) { - this.mimeType = mimeType; - this.content = content; - } - - @Override - public String getMimeType() { - return this.mimeType; - } - - @Override - public ByteBuf getMetadata() { - return this.content; - } - } - - static final class CompressedTypeEntry implements Entry { - - private final WellKnownMimeType mimeType; - private final ByteBuf content; - - CompressedTypeEntry(WellKnownMimeType mimeType, ByteBuf content) { - this.mimeType = mimeType; - this.content = content; - } - - @Override - public String getMimeType() { - return this.mimeType.getMime(); - } - - @Override - public ByteBuf getMetadata() { - return this.content; - } - } - - /** - * A {@link Entry#isPassthrough() pass-through} {@link Entry} that encapsulates a - * compressed-mime metadata that couldn't be recognized. - */ - public static final class UnknownCompressedTypeEntry implements Entry { - - private final byte identifier; - private final ByteBuf content; - - UnknownCompressedTypeEntry(byte identifier, ByteBuf content) { - this.identifier = identifier; - this.content = content; - } - - @Override - public boolean isPassthrough() { - return true; - } - - @Override - public String getMimeType() { - return WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE.getMime(); - } - - /** - * Return the compressed identifier that was used in the original decoded metadata, but couldn't be - * decoded because this implementation only knows this as "reserved for future use". The original - */ - public byte getUnknownReservedId() { - return this.identifier; - } - - @Override - public ByteBuf getMetadata() { - return this.content; - } - } - -} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java index a9e729b9b..4427379cd 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java @@ -192,8 +192,8 @@ public static void encodeAndAddMetadata(CompositeByteBuf compositeMetaData, Byte // see #encodeMetadataHeader(ByteBufAllocator, byte, int) public static void encodeAndAddMetadata(CompositeByteBuf compositeMetaData, ByteBufAllocator allocator, WellKnownMimeType knownMimeType, ByteBuf metadata) { compositeMetaData.addComponents(true, - encodeMetadataHeader(allocator, knownMimeType.getIdentifier(), metadata.readableBytes()), - metadata); + encodeMetadataHeader(allocator, knownMimeType.getIdentifier(), + metadata.readableBytes()), metadata); } /** @@ -207,8 +207,239 @@ public static void encodeAndAddMetadata(CompositeByteBuf compositeMetaData, Byte // see #encodeMetadataHeader(ByteBufAllocator, byte, int) static void encodeAndAddMetadata(CompositeByteBuf compositeMetaData, ByteBufAllocator allocator, byte unknownCompressedMimeType, ByteBuf metadata) { compositeMetaData.addComponents(true, - encodeMetadataHeader(allocator, unknownCompressedMimeType, metadata.readableBytes()), - metadata); + encodeMetadataHeader(allocator, unknownCompressedMimeType, + metadata.readableBytes()), metadata); + } + + //=== ENTRY === + + /** + * An entry in a Composite Metadata, which exposes the {@link #getMimeType() mime type} and {@link ByteBuf} + * {@link #getMetadata() content} of the metadata entry. + */ + public interface Entry { + + /** + * Create an {@link Entry} from a {@link WellKnownMimeType}. This will be encoded in a compressed format that + * uses the {@link WellKnownMimeType#getIdentifier() mime identifier}. + * + * @param mimeType the {@link WellKnownMimeType} to use for the entry + * @param metadataContentBuffer the content {@link ByteBuf} to use for the entry + * @return the new entry + */ + public static Entry wellKnownMime(WellKnownMimeType mimeType, ByteBuf metadataContentBuffer) { + return new CompressedTypeEntry(mimeType, metadataContentBuffer); + } + + /** + * Create an {@link Entry} from a custom mime type represented as an US-ASCII only {@link String}. + * The whole literal mime type will thus be encoded. + * + * @param mimeType the custom mime type {@link String} + * @param metadataContentBuffer the content {@link ByteBuf} to use for the entry + * @return the new entry + */ + public static Entry customMime(String mimeType, ByteBuf metadataContentBuffer) { + return new CustomTypeEntry(mimeType, metadataContentBuffer); + } + + /** + * Create an {@link Entry} from an unrecognized yet valid "well-known" mime type, ie. a {@code byte} that would map + * to {@link WellKnownMimeType#UNKNOWN_RESERVED_MIME_TYPE}. Prefer using {@link #wellKnownMime(WellKnownMimeType, ByteBuf)} + * if the mime code is recognizable by this client. + *

+ * This case would usually be encountered when decoding a composite metadata entry from a remote that uses a more recent + * version of the {@link WellKnownMimeType} extension, and this method can be useful to create an unprocessed entry + * in such a case, ensuring no information is lost when forwarding frames. + * + * @param mimeCode the reserved but unrecognized compressed mime type {@code byte} + * @param metadataContentBuffer the content {@link ByteBuf} to use for the entry + * @return the new entry + * @see #wellKnownMime(WellKnownMimeType, ByteBuf) + */ + public static Entry rawCompressedMime(byte mimeCode, ByteBuf metadataContentBuffer) { + return new UnknownCompressedTypeEntry(mimeCode, metadataContentBuffer); + } + + /** + * Incrementally decode the next metadata entry from a {@link ByteBuf} into an {@link Entry}. + * This is only possible on frame types used to initiate + * interactions, if the SETUP metadata mime type was {@link WellKnownMimeType#MESSAGE_RSOCKET_COMPOSITE_METADATA}. + *

+ * Each entry {@link ByteBuf} is a {@link ByteBuf#readSlice(int) slice} of the original buffer that can also be + * {@link ByteBuf#readRetainedSlice(int) retained} if needed. + * + * @param buffer the buffer to decode + * @param retainMetadataSlices should each slide be retained when read from the original buffer? + * @return the decoded {@link Entry} + */ + static Entry decodeEntry(ByteBuf buffer, boolean retainMetadataSlices) { + ByteBuf[] entry = decodeMimeAndContentBuffers(buffer, retainMetadataSlices); + if (entry == METADATA_MALFORMED) { + throw new IllegalArgumentException("composite metadata entry buffer is too short to contain proper entry"); + } + if (entry == METADATA_BUFFERS_DONE) { + return null; + } + + ByteBuf encodedHeader = entry[0]; + ByteBuf metadataContent = entry[1]; + + + //the flyweight already validated the size of the buffer, + //this is only to distinguish id vs custom type + if (encodedHeader.readableBytes() == 1) { + //id + byte id = decodeMimeIdFromMimeBuffer(encodedHeader); + WellKnownMimeType wkn = WellKnownMimeType.fromId(id); + if (wkn == WellKnownMimeType.UNPARSEABLE_MIME_TYPE) { + //should not happen due to flyweight's decodeEntry own guard + throw new IllegalStateException("composite metadata entry parsing failed on compressed mime id " + id); + } + if (wkn == WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE) { + return new UnknownCompressedTypeEntry(id, metadataContent); + } + return new CompressedTypeEntry(wkn, metadataContent); + } + else { + CharSequence customMimeCharSequence = decodeMimeTypeFromMimeBuffer(encodedHeader); + if (customMimeCharSequence == null) { + //should not happen due to flyweight's decodeEntry own guard + throw new IllegalArgumentException("composite metadata entry parsing failed on custom type"); + } + return new CustomTypeEntry(customMimeCharSequence.toString(), metadataContent); + } + } + + /** + * A passthrough entry is one for which the {@link #getMimeType()} could not be decoded. + * This is usually because it was compressed on the wire, but using an id that is still just "reserved for + * future use" in this implementation. + *

+ * Still, another actor on the network might be able to interpret such an entry, which should thus be + * re-encoded as it was when forwarding the frame. + *

+ * The {@link #getMetadata()} exposes the raw content buffer of the entry (as any other entry). + * + * @return true if this entry should be ignored but passed through as is during re-encoding + */ + default boolean isPassthrough() { + return false; + } + + /** + * @return the mime type for this entry + */ + @Nullable + String getMimeType(); + + /** + * @return the metadata content of this entry + */ + ByteBuf getMetadata(); + + /** + * Encode this {@link Entry} into a {@link CompositeByteBuf} representing a composite metadata. + * This buffer may already hold components for previous {@link Entry entries}. + * + * @param compositeByteBuf the {@link CompositeByteBuf} to hold the components of the whole composite metadata + * @param byteBufAllocator the {@link ByteBufAllocator} to use to allocate new buffers as needed + */ + default void encodeInto(CompositeByteBuf compositeByteBuf, ByteBufAllocator byteBufAllocator) { + if (this instanceof CompressedTypeEntry) { + CompressedTypeEntry cte = (CompressedTypeEntry) this; + encodeAndAddMetadata(compositeByteBuf, byteBufAllocator, + cte.mimeType, cte.content); + } + else if (this instanceof UnknownCompressedTypeEntry) { + UnknownCompressedTypeEntry ucte = (UnknownCompressedTypeEntry) this; + encodeAndAddMetadata(compositeByteBuf, byteBufAllocator, + (byte) ucte.identifier, ucte.content); + } + else { + encodeAndAddMetadata(compositeByteBuf, byteBufAllocator, + getMimeType(), getMetadata()); + } + } + } + + static final class CustomTypeEntry implements Entry { + + private final String mimeType; + private final ByteBuf content; + + CustomTypeEntry(String mimeType, ByteBuf content) { + this.mimeType = mimeType; + this.content = content; + } + + @Override + public String getMimeType() { + return this.mimeType; + } + + @Override + public ByteBuf getMetadata() { + return this.content; + } + } + + static final class CompressedTypeEntry implements Entry { + + private final WellKnownMimeType mimeType; + private final ByteBuf content; + + CompressedTypeEntry(WellKnownMimeType mimeType, ByteBuf content) { + this.mimeType = mimeType; + this.content = content; + } + + @Override + public String getMimeType() { + return this.mimeType.getMime(); + } + + @Override + public ByteBuf getMetadata() { + return this.content; + } } + /** + * A {@link Entry#isPassthrough() pass-through} {@link Entry} that encapsulates a + * compressed-mime metadata that couldn't be recognized. + */ + static final class UnknownCompressedTypeEntry implements Entry { + + private final byte identifier; + private final ByteBuf content; + + UnknownCompressedTypeEntry(byte identifier, ByteBuf content) { + this.identifier = identifier; + this.content = content; + } + + @Override + public boolean isPassthrough() { + return true; + } + + @Override + public String getMimeType() { + return WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE.getMime(); + } + + /** + * Return the compressed identifier that was used in the original decoded metadata, but couldn't be + * decoded because this implementation only knows this as "reserved for future use". The original + */ + public byte getUnknownReservedId() { + return this.identifier; + } + + @Override + public ByteBuf getMetadata() { + return this.content; + } + } } diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java index fdd41e33b..7452d7244 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java @@ -172,7 +172,7 @@ void customMimeHeaderLengthOne() { assertThat((int) header.readByte()) .as("mime length") - .isEqualTo(1 -1); //encoded as actual length - 1 + .isZero(); //encoded as actual length - 1 assertThat(header.readCharSequence(1, CharsetUtil.US_ASCII)) .as("mime string") diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java deleted file mode 100644 index 81e84c9f8..000000000 --- a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java +++ /dev/null @@ -1,301 +0,0 @@ -package io.rsocket.metadata; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.CompositeByteBuf; -import io.netty.util.CharsetUtil; -import io.rsocket.metadata.CompositeMetadata.CompressedTypeEntry; -import io.rsocket.metadata.CompositeMetadata.CustomTypeEntry; -import io.rsocket.metadata.CompositeMetadata.Entry; -import io.rsocket.metadata.CompositeMetadata.UnknownCompressedTypeEntry; -import io.rsocket.test.util.ByteBufUtils; -import io.rsocket.util.NumberUtils; -import org.junit.jupiter.api.Test; - -import java.util.Arrays; - -import static io.netty.util.CharsetUtil.UTF_8; -import static org.assertj.core.api.Assertions.*; - -class CompositeMetadataTest { - - @Test - void decodeCompositeMetadata() { - //metadata 1: well known - WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; - ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); - metadata1.writeCharSequence("abcdefghijkl", UTF_8); - - //metadata 2: custom - String mimeType2 = "application/custom"; - ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); - metadata2.writeChar('E'); - metadata2.writeChar('∑'); - metadata2.writeChar('é'); - metadata2.writeBoolean(true); - metadata2.writeChar('W'); - - //metadata 3: reserved but unknown - byte reserved = 120; - assertThat(WellKnownMimeType.fromId(reserved)) - .as("ensure UNKNOWN RESERVED used in test") - .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); - ByteBuf metadata3 = ByteBufAllocator.DEFAULT.buffer(); - metadata3.writeByte(88); - - CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); - CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); - CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, reserved, metadata3); - - CompositeMetadata metadata = CompositeMetadata.decodeComposite(compositeMetadata); - - assertThat(metadata.size()).as("size").isEqualTo(3); - - assertThat(metadata.get(0)) - .satisfies(e -> assertThat(e.getMimeType()) - .as("metadata1 mime") - .isEqualTo(WellKnownMimeType.APPLICATION_PDF.getMime()) - ) - .satisfies(e -> assertThat(e.getMetadata().toString(UTF_8)) - .as("metadata1 decoded") - .isEqualTo("abcdefghijkl") - ); - - System.out.println(ByteBufUtil.hexDump(metadata.get(1).getMetadata())); - - assertThat(metadata.get(1)) - .satisfies(e -> assertThat(e.getMimeType()) - .as("metadata2 mime") - .isEqualTo(mimeType2) - ) - .satisfies(e -> assertThat(e.getMetadata()) - .as("metadata2 buffer") - .isEqualByComparingTo(metadata2) - ); - - assertThat(metadata.get(2)) - .matches(Entry::isPassthrough) - .isInstanceOf(UnknownCompressedTypeEntry.class) - .satisfies(e -> assertThat(e.getMimeType()).isEqualTo(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE.getMime())) - .satisfies(e -> assertThat(((UnknownCompressedTypeEntry) e).getUnknownReservedId()) - .isEqualTo(reserved)); - } - - @Test - void encodeEntryWellKnownMetadata() { - WellKnownMimeType type = WellKnownMimeType.fromId(5); - //5 = 0b00000101 - byte expected = (byte) 0b10000101; - - ByteBuf content = ByteBufUtils.getRandomByteBuf(2); - Entry entry = new CompressedTypeEntry(type, content); - - final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - CompositeMetadata.encodeEntry(ByteBufAllocator.DEFAULT, metadata, entry); - - assertThat(metadata.readByte()) - .as("mime header") - .isEqualTo(expected); - assertThat(metadata.readUnsignedMedium()).as("length header").isEqualTo(2); - assertThat(metadata.readSlice(2)).as("content").isEqualByComparingTo(content); - } - - @Test - void encodeEntryCustomMetadata() { - // length 3, encoded as length - 1 since 0 is not authorized - byte expected = (byte) 2; - ByteBuf content = ByteBufUtils.getRandomByteBuf(2); - Entry entry = new CustomTypeEntry("foo", content); - - final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - CompositeMetadata.encodeEntry(ByteBufAllocator.DEFAULT, metadata, entry); - - assertThat(metadata.readByte()) - .as("mime header") - .isEqualTo(expected); - assertThat(metadata.readCharSequence(3, CharsetUtil.US_ASCII).toString()) - .isEqualTo("foo"); - assertThat(metadata.readUnsignedMedium()).as("length header").isEqualTo(2); - assertThat(metadata.readSlice(2)).as("content").isEqualByComparingTo(content); - } - - @Test - void encodeEntryPassthroughMetadata() { - //120 = 0b01111000 - byte expected = (byte) 0b11111000; - - ByteBuf content = ByteBufUtils.getRandomByteBuf(2); - UnknownCompressedTypeEntry entry = new UnknownCompressedTypeEntry((byte) 120, content); - - final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - CompositeMetadata.encodeEntry(ByteBufAllocator.DEFAULT, metadata, entry); - - assertThat(metadata.readByte()) - .as("mime header") - .isEqualTo(expected); - assertThat(metadata.readUnsignedMedium()).as("length header").isEqualTo(2); - assertThat(metadata.readSlice(2)).as("content").isEqualByComparingTo(content); - } - - @Test - void encodeCompositeMetadata() { - final Entry entry1 = new CustomTypeEntry("foo", - ByteBufUtils.getRandomByteBuf(1)); - - WellKnownMimeType mime2 = WellKnownMimeType.fromId(5); - //5 = 0b00000101 - byte expected2 = (byte) 0b10000101; - final Entry entry2 = new CompressedTypeEntry(mime2, - ByteBufUtils.getRandomByteBuf(2)); - - byte id3 = (byte) 120; - byte expected3 = (byte) 0b11111000; - final Entry entry3 = new UnknownCompressedTypeEntry(id3, - ByteBufUtils.getRandomByteBuf(3)); - - CompositeMetadata compositeMetadata = new CompositeMetadata(Arrays.asList(entry1, entry2, entry3)); - CompositeByteBuf buf = CompositeMetadata.encodeComposite(ByteBufAllocator.DEFAULT, compositeMetadata); - - assertThat(buf.readByte()) - .as("meta1 mime length") - .isEqualTo((byte) 2); - assertThat(buf.readCharSequence(3, CharsetUtil.US_ASCII).toString()) - .as("meta1 mime") - .isEqualTo("foo"); - assertThat(buf.readUnsignedMedium()) - .as("meta1 content length") - .isEqualTo(1); - assertThat(buf.readBytes(1)) - .as("meta1 content") - .isEqualByComparingTo(entry1.getMetadata()); - - assertThat(buf.readByte()) - .as("meta2 id") - .isEqualTo(expected2); - assertThat(buf.readUnsignedMedium()) - .as("meta2 content length") - .isEqualTo(2); - assertThat(buf.readBytes(2)) - .as("meta2 content") - .isEqualByComparingTo(entry2.getMetadata()); - - assertThat(buf.readByte()) - .as("meta3 id") - .isEqualTo(expected3); - assertThat(buf.readUnsignedMedium()) - .as("meta3 content length") - .isEqualTo(3); - assertThat(buf.readBytes(3)) - .as("meta3 content") - .isEqualByComparingTo(entry3.getMetadata()); - } - - @Test - void getForTypeWithTwoMatches() { - ByteBuf noMatch = ByteBufUtils.getRandomByteBuf(2); - ByteBuf match1 = ByteBufUtils.getRandomByteBuf(2); - ByteBuf match2 = ByteBufUtils.getRandomByteBuf(2); - CompositeMetadata metadata = new CompositeMetadata(Arrays.asList( - new CustomTypeEntry("noMatch", noMatch), - new CustomTypeEntry("match", match1), - new CustomTypeEntry("match", match2) - )); - - assertThat(metadata.get("match")) - .isNotNull() - .extracting(Entry::getMetadata) - .isSameAs(match1); - assertThat(metadata.getAll("match")) - .flatExtracting(Entry::getMetadata) - .containsExactly(match1, match2); - } - - @Test - void getForTypeWithNoMatch() { - ByteBuf noMatch1 = ByteBufUtils.getRandomByteBuf(2); - ByteBuf noMatch2 = ByteBufUtils.getRandomByteBuf(2); - CompositeMetadata metadata = new CompositeMetadata(Arrays.asList( - new CustomTypeEntry("noMatch1", noMatch1), - new CustomTypeEntry("noMatch2", noMatch2) - )); - - assertThat(metadata.get("match")).isNull(); - assertThat(metadata.getAll("match")) - .isEmpty(); - } - - @Test - void getAllForTypeIsUnmodifiable() { - ByteBuf match1 = ByteBufUtils.getRandomByteBuf(2); - ByteBuf match2 = ByteBufUtils.getRandomByteBuf(2); - CompositeMetadata metadata = new CompositeMetadata(Arrays.asList( - new CustomTypeEntry("match1", match1), - new CustomTypeEntry("match2", match2) - )); - - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> metadata.getAll("match").clear()); - } - - @Test - void getAllParseableVsGetAll() { - final Entry entry1 = new CustomTypeEntry("foo", ByteBufUtils.getRandomByteBuf(2)); - final Entry entry2 = new CompressedTypeEntry(WellKnownMimeType.APPLICATION_GZIP, ByteBufUtils.getRandomByteBuf(2)); - final Entry entry3 = new UnknownCompressedTypeEntry((byte) 120, ByteBufUtils.getRandomByteBuf(2)); - - CompositeMetadata metadata = new CompositeMetadata( - Arrays.asList(entry1, entry2, entry3)); - - assertThat(metadata.getAll()) - .as("getAll()") - .containsExactly(entry1, entry2, entry3); - assertThat(metadata.getAllParseable()) - .as("getAllParseable()") - .containsExactly(entry1, entry2); - } - - @Test - void decodeEntryTooShortForMimeLength() { - ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); - fakeEntry.writeByte(120); - - assertThatIllegalArgumentException() - .isThrownBy(() -> CompositeMetadata.decodeEntry(fakeEntry, false)) - .withMessage("composite metadata entry buffer is too short to contain proper entry"); - } - - @Test - void decodeEntryHasNoContentLength() { - ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); - fakeEntry.writeByte(0); - fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); - - assertThatIllegalArgumentException() - .isThrownBy(() -> CompositeMetadata.decodeEntry(fakeEntry, false)) - .withMessage("composite metadata entry buffer is too short to contain proper entry"); - } - - @Test - void decodeEntryTooShortForContentLength() { - ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); - fakeEntry.writeByte(1); - fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); - NumberUtils.encodeUnsignedMedium(fakeEntry, 456); - fakeEntry.writeChar('w'); - - assertThatIllegalArgumentException() - .isThrownBy(() -> CompositeMetadata.decodeEntry(fakeEntry, false)) - .withMessage("composite metadata entry buffer is too short to contain proper entry"); - } - - @Test - void decodeEntryOnDoneBufferReturnsNull() { - ByteBuf fakeBuffer = ByteBufUtils.getRandomByteBuf(0); - - assertThat(CompositeMetadata.decodeEntry(fakeBuffer, false)) - .as("empty entry") - .isNull(); - } -} \ No newline at end of file diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java new file mode 100644 index 000000000..aec8204a1 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java @@ -0,0 +1,117 @@ +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.util.CharsetUtil; +import io.rsocket.metadata.CompositeMetadataFlyweight.Entry; +import io.rsocket.metadata.CompositeMetadataFlyweight.Entry.CompressedTypeEntry; +import io.rsocket.metadata.CompositeMetadataFlyweight.Entry.CustomTypeEntry; +import io.rsocket.test.util.ByteBufUtils; +import io.rsocket.util.NumberUtils; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class EntryTest { + + @Test + void encodeEntryWellKnownMetadata() { + WellKnownMimeType type = WellKnownMimeType.fromId(5); + //5 = 0b00000101 + byte expected = (byte) 0b10000101; + + ByteBuf content = ByteBufUtils.getRandomByteBuf(2); + Entry entry = new CompressedTypeEntry(type, content); + + final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + entry.encodeInto(metadata, ByteBufAllocator.DEFAULT); + + assertThat(metadata.readByte()) + .as("mime header") + .isEqualTo(expected); + assertThat(metadata.readUnsignedMedium()).as("length header").isEqualTo(2); + assertThat(metadata.readSlice(2)).as("content").isEqualByComparingTo(content); + } + + @Test + void encodeEntryCustomMetadata() { + // length 3, encoded as length - 1 since 0 is not authorized + byte expected = (byte) 2; + ByteBuf content = ByteBufUtils.getRandomByteBuf(2); + Entry entry = new CustomTypeEntry("foo", content); + + final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + entry.encodeInto(metadata, ByteBufAllocator.DEFAULT); + + assertThat(metadata.readByte()) + .as("mime header") + .isEqualTo(expected); + assertThat(metadata.readCharSequence(3, CharsetUtil.US_ASCII).toString()) + .isEqualTo("foo"); + assertThat(metadata.readUnsignedMedium()).as("length header").isEqualTo(2); + assertThat(metadata.readSlice(2)).as("content").isEqualByComparingTo(content); + } + + @Test + void encodeEntryPassthroughMetadata() { + //120 = 0b01111000 + byte expected = (byte) 0b11111000; + + ByteBuf content = ByteBufUtils.getRandomByteBuf(2); + Entry.UnknownCompressedTypeEntry entry = new Entry.UnknownCompressedTypeEntry((byte) 120, content); + + final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + entry.encodeInto(metadata, ByteBufAllocator.DEFAULT); + + assertThat(metadata.readByte()) + .as("mime header") + .isEqualTo(expected); + assertThat(metadata.readUnsignedMedium()).as("length header").isEqualTo(2); + assertThat(metadata.readSlice(2)).as("content").isEqualByComparingTo(content); + } + + @Test + void decodeEntryTooShortForMimeLength() { + ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + fakeEntry.writeByte(120); + + assertThatIllegalArgumentException() + .isThrownBy(() -> Entry.decodeEntry(fakeEntry, false)) + .withMessage("composite metadata entry buffer is too short to contain proper entry"); + } + + @Test + void decodeEntryHasNoContentLength() { + ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + fakeEntry.writeByte(0); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); + + assertThatIllegalArgumentException() + .isThrownBy(() -> Entry.decodeEntry(fakeEntry, false)) + .withMessage("composite metadata entry buffer is too short to contain proper entry"); + } + + @Test + void decodeEntryTooShortForContentLength() { + ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + fakeEntry.writeByte(1); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); + NumberUtils.encodeUnsignedMedium(fakeEntry, 456); + fakeEntry.writeChar('w'); + + assertThatIllegalArgumentException() + .isThrownBy(() -> Entry.decodeEntry(fakeEntry, false)) + .withMessage("composite metadata entry buffer is too short to contain proper entry"); + } + + @Test + void decodeEntryOnDoneBufferReturnsNull() { + ByteBuf fakeBuffer = ByteBufUtils.getRandomByteBuf(0); + + assertThat(Entry.decodeEntry(fakeBuffer, false)) + .as("empty entry") + .isNull(); + } +} \ No newline at end of file From 644976aa75187e871bc05bb9a901a15d5ed761c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 3 Jun 2019 15:25:11 +0200 Subject: [PATCH 13/25] Add javadoc for CompositeMetadataFlyweight methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Simon Baslé --- .../metadata/CompositeMetadataFlyweight.java | 59 +++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java index 4427379cd..f230b5250 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java @@ -8,6 +8,11 @@ import io.rsocket.util.NumberUtils; import reactor.util.annotation.Nullable; +/** + * A flyweight class that can be used to encode/decode composite metadata information to/from {@link ByteBuf}. + * This is intended for low-level efficient manipulation of such buffers, but each composite metadata entry can be also + * manipulated as an higher abstraction {@link Entry} class, which provides its own encoding and decoding primitives. + */ public class CompositeMetadataFlyweight { static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 @@ -34,7 +39,29 @@ public class CompositeMetadataFlyweight { private CompositeMetadataFlyweight() {} - //TODO document how to go from ByteBuf to id/string mime and distinguish + /** + * Decode the next metadata entry (a mime header + content pair of {@link ByteBuf}) from a {@link ByteBuf} that contains + * at least enough bytes for one more such entry. + * The header buffer is either: + *

    + *
  • + * made up of a single byte: this represents an encoded mime id, which can be further decoded using {@link #decodeMimeIdFromMimeBuffer(ByteBuf)} + *
  • + *
  • + * made up of 2 or more bytes: this represents an encoded mime String + its length, which can be further decoded + * using {@link #decodeMimeTypeFromMimeBuffer(ByteBuf)}. Note the encoded length, in the first byte, is skipped + * by this decoding method because the remaining length of the buffer is that of the mime string. + *
  • + *
+ * Moreover, if the source buffer is empty of readable bytes it is assumed that the composite has been decoded entirely + * and the {@link #METADATA_BUFFERS_DONE} constant is returned. If the buffer contains some readable bytes but + * not enough for a correct representation of an entry, the {@link #METADATA_MALFORMED} constant is returned. + * + * @param compositeMetadata the source {@link ByteBuf} that originally contains one or more metadata entries + * @param retainSlices should produced metadata entry buffers {@link ByteBuf#slice() slices} be {@link ByteBuf#retainedSlice() retained}? + * @return a {@link ByteBuf} slice array of length 2 containing the mime header buffer and the content buffer, or one + * of the zero-length error constant arrays + */ public static ByteBuf[] decodeMimeAndContentBuffers(ByteBuf compositeMetadata, boolean retainSlices) { if (compositeMetadata.isReadable()) { ByteBuf mime; @@ -89,8 +116,18 @@ public static ByteBuf[] decodeMimeAndContentBuffers(ByteBuf compositeMetadata, b return METADATA_BUFFERS_DONE; } - //TODO document how to check buffer is indeed an id - //TODO document error conditions + /** + * Decode a {@code byte} compressed mime id from a {@link ByteBuf}, assuming said buffer properly contains such an id. + *

+ * The buffer must have exactly one readable byte, which is assumed to have been tested for mime id encoding via + * the {@link #STREAM_METADATA_KNOWN_MASK} mask ({@code firstByte & STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK}). + *

+ * If there is no readable byte, the negative identifier of {@link WellKnownMimeType#UNPARSEABLE_MIME_TYPE} is returned. + * + * @param mimeBuffer the buffer that should next contain the compressed mime id byte + * @return the compressed mime id, between 0 and 127, or a negative id if the input is invalid + * @see #decodeMimeTypeFromMimeBuffer(ByteBuf) + */ public static byte decodeMimeIdFromMimeBuffer(ByteBuf mimeBuffer) { if (mimeBuffer.readableBytes() != 1) { return WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier(); @@ -98,7 +135,21 @@ public static byte decodeMimeIdFromMimeBuffer(ByteBuf mimeBuffer) { return (byte) (mimeBuffer.readByte() & STREAM_METADATA_LENGTH_MASK); } - //TODO document error conditions + /** + * Decode a {@link CharSequence} custome mime type from a {@link ByteBuf}, assuming said buffer properly contains such + * a mime type. + *

+ * The buffer must at least have two readable bytes, which distinguishes it from the {@link #decodeMimeIdFromMimeBuffer(ByteBuf) compressed id} + * case. The first byte is a size and the remaining bytes must correspond to the {@link CharSequence}, encoded fully + * in US_ASCII. As a result, the first byte can simply be skipped, and the remaining of the buffer be decoded to + * the mime type. + *

+ * If the mime header buffer is less than 2 bytes long, returns {@code null}. + * + * @param flyweightMimeBuffer the mime header {@link ByteBuf} that contains length + custom mime type + * @return the decoded custom mime type, as a {@link CharSequence}, or null if the input is invalid + * @see #decodeMimeIdFromMimeBuffer(ByteBuf) + */ @Nullable public static CharSequence decodeMimeTypeFromMimeBuffer(ByteBuf flyweightMimeBuffer) { if (flyweightMimeBuffer.readableBytes() < 2) { From 90eafaa98e70731852aa938d753a8e409e3ff042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 3 Jun 2019 16:04:44 +0200 Subject: [PATCH 14/25] Switch to a single Entry implementation, remove isPassthrough MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Simon Baslé --- .../metadata/CompositeMetadataFlyweight.java | 157 +++++------------- .../java/io/rsocket/metadata/EntryTest.java | 141 +++++++++++++++- 2 files changed, 182 insertions(+), 116 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java index f230b5250..498a6e77e 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java @@ -267,8 +267,19 @@ static void encodeAndAddMetadata(CompositeByteBuf compositeMetaData, ByteBufAllo /** * An entry in a Composite Metadata, which exposes the {@link #getMimeType() mime type} and {@link ByteBuf} * {@link #getMetadata() content} of the metadata entry. + *

+ * There is one case where the entry cannot really be used other than by forwarding it to another client: when the + * mime type is represented as a compressed {@code byte} id, but said id is only identified as "reserved" in the + * current implementation ({@link WellKnownMimeType#UNKNOWN_RESERVED_MIME_TYPE}). + * In that case, the corresponding {@link Entry} should reflect that by having a {@code null} {@link #getMimeType()} + * along a positive {@link #getMimeId()}. + *

+ * Non-null {@link #getMimeType()} along with positive {@link #getMimeId()} denote a compressed mime metadata entry, + * whereas the same with a negative {@link #getMimeId()} would denote a custom mime type metadata entry. + *

+ * In all three cases, the {@link #getMetadata()} expose the content of the metadata entry as a raw {@link ByteBuf}. */ - public interface Entry { + public static class Entry { /** * Create an {@link Entry} from a {@link WellKnownMimeType}. This will be encoded in a compressed format that @@ -279,7 +290,7 @@ public interface Entry { * @return the new entry */ public static Entry wellKnownMime(WellKnownMimeType mimeType, ByteBuf metadataContentBuffer) { - return new CompressedTypeEntry(mimeType, metadataContentBuffer); + return new Entry(mimeType.getMime(), mimeType.getIdentifier(), metadataContentBuffer); } /** @@ -291,7 +302,7 @@ public static Entry wellKnownMime(WellKnownMimeType mimeType, ByteBuf metadataCo * @return the new entry */ public static Entry customMime(String mimeType, ByteBuf metadataContentBuffer) { - return new CustomTypeEntry(mimeType, metadataContentBuffer); + return new Entry(mimeType, (byte) -1, metadataContentBuffer); } /** @@ -309,7 +320,7 @@ public static Entry customMime(String mimeType, ByteBuf metadataContentBuffer) { * @see #wellKnownMime(WellKnownMimeType, ByteBuf) */ public static Entry rawCompressedMime(byte mimeCode, ByteBuf metadataContentBuffer) { - return new UnknownCompressedTypeEntry(mimeCode, metadataContentBuffer); + return new Entry(null, mimeCode, metadataContentBuffer); } /** @@ -348,9 +359,9 @@ static Entry decodeEntry(ByteBuf buffer, boolean retainMetadataSlices) { throw new IllegalStateException("composite metadata entry parsing failed on compressed mime id " + id); } if (wkn == WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE) { - return new UnknownCompressedTypeEntry(id, metadataContent); + return new Entry(null, id, metadataContent); } - return new CompressedTypeEntry(wkn, metadataContent); + return new Entry(wkn.getMime(), wkn.getIdentifier(), metadataContent); } else { CharSequence customMimeCharSequence = decodeMimeTypeFromMimeBuffer(encodedHeader); @@ -358,36 +369,46 @@ static Entry decodeEntry(ByteBuf buffer, boolean retainMetadataSlices) { //should not happen due to flyweight's decodeEntry own guard throw new IllegalArgumentException("composite metadata entry parsing failed on custom type"); } - return new CustomTypeEntry(customMimeCharSequence.toString(), metadataContent); + return new Entry(customMimeCharSequence.toString(), (byte) -1, metadataContent); } } + private final String mimeString; + private final byte mimeCode; + private final ByteBuf content; + + public Entry(@Nullable String mimeString, byte mimeCode, ByteBuf content) { + this.mimeString = mimeString; + this.mimeCode = mimeCode; + this.content = content; + } + /** - * A passthrough entry is one for which the {@link #getMimeType()} could not be decoded. - * This is usually because it was compressed on the wire, but using an id that is still just "reserved for - * future use" in this implementation. - *

- * Still, another actor on the network might be able to interpret such an entry, which should thus be - * re-encoded as it was when forwarding the frame. + * Returns the mime type {@link String} representation if there is one. *

- * The {@link #getMetadata()} exposes the raw content buffer of the entry (as any other entry). + * A {@code null} value should only occur with a positive {@link #getMimeId()}, + * denoting an entry that is compressed but unparseable (see {@link #rawCompressedMime(byte, ByteBuf)}). * - * @return true if this entry should be ignored but passed through as is during re-encoding + * @return the mime type for this entry, or null */ - default boolean isPassthrough() { - return false; + @Nullable + public String getMimeType() { + return this.mimeString; } /** - * @return the mime type for this entry + * @return the compressed mime id byte if relevant (0-127), or -1 if not */ - @Nullable - String getMimeType(); + public byte getMimeId() { + return this.mimeCode; + } /** * @return the metadata content of this entry */ - ByteBuf getMetadata(); + public ByteBuf getMetadata() { + return this.content; + } /** * Encode this {@link Entry} into a {@link CompositeByteBuf} representing a composite metadata. @@ -396,101 +417,15 @@ default boolean isPassthrough() { * @param compositeByteBuf the {@link CompositeByteBuf} to hold the components of the whole composite metadata * @param byteBufAllocator the {@link ByteBufAllocator} to use to allocate new buffers as needed */ - default void encodeInto(CompositeByteBuf compositeByteBuf, ByteBufAllocator byteBufAllocator) { - if (this instanceof CompressedTypeEntry) { - CompressedTypeEntry cte = (CompressedTypeEntry) this; - encodeAndAddMetadata(compositeByteBuf, byteBufAllocator, - cte.mimeType, cte.content); - } - else if (this instanceof UnknownCompressedTypeEntry) { - UnknownCompressedTypeEntry ucte = (UnknownCompressedTypeEntry) this; + public void encodeInto(CompositeByteBuf compositeByteBuf, ByteBufAllocator byteBufAllocator) { + if (this.mimeCode >= 0) { encodeAndAddMetadata(compositeByteBuf, byteBufAllocator, - (byte) ucte.identifier, ucte.content); + this.mimeCode, this.content); } else { encodeAndAddMetadata(compositeByteBuf, byteBufAllocator, - getMimeType(), getMetadata()); + this.mimeString, this.content); } } } - - static final class CustomTypeEntry implements Entry { - - private final String mimeType; - private final ByteBuf content; - - CustomTypeEntry(String mimeType, ByteBuf content) { - this.mimeType = mimeType; - this.content = content; - } - - @Override - public String getMimeType() { - return this.mimeType; - } - - @Override - public ByteBuf getMetadata() { - return this.content; - } - } - - static final class CompressedTypeEntry implements Entry { - - private final WellKnownMimeType mimeType; - private final ByteBuf content; - - CompressedTypeEntry(WellKnownMimeType mimeType, ByteBuf content) { - this.mimeType = mimeType; - this.content = content; - } - - @Override - public String getMimeType() { - return this.mimeType.getMime(); - } - - @Override - public ByteBuf getMetadata() { - return this.content; - } - } - - /** - * A {@link Entry#isPassthrough() pass-through} {@link Entry} that encapsulates a - * compressed-mime metadata that couldn't be recognized. - */ - static final class UnknownCompressedTypeEntry implements Entry { - - private final byte identifier; - private final ByteBuf content; - - UnknownCompressedTypeEntry(byte identifier, ByteBuf content) { - this.identifier = identifier; - this.content = content; - } - - @Override - public boolean isPassthrough() { - return true; - } - - @Override - public String getMimeType() { - return WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE.getMime(); - } - - /** - * Return the compressed identifier that was used in the original decoded metadata, but couldn't be - * decoded because this implementation only knows this as "reserved for future use". The original - */ - public byte getUnknownReservedId() { - return this.identifier; - } - - @Override - public ByteBuf getMetadata() { - return this.content; - } - } } diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java index aec8204a1..ae0b5507a 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java @@ -5,8 +5,6 @@ import io.netty.buffer.CompositeByteBuf; import io.netty.util.CharsetUtil; import io.rsocket.metadata.CompositeMetadataFlyweight.Entry; -import io.rsocket.metadata.CompositeMetadataFlyweight.Entry.CompressedTypeEntry; -import io.rsocket.metadata.CompositeMetadataFlyweight.Entry.CustomTypeEntry; import io.rsocket.test.util.ByteBufUtils; import io.rsocket.util.NumberUtils; import org.junit.jupiter.api.Test; @@ -23,7 +21,7 @@ void encodeEntryWellKnownMetadata() { byte expected = (byte) 0b10000101; ByteBuf content = ByteBufUtils.getRandomByteBuf(2); - Entry entry = new CompressedTypeEntry(type, content); + Entry entry = new Entry(type.getMime(), type.getIdentifier(), content); final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); entry.encodeInto(metadata, ByteBufAllocator.DEFAULT); @@ -40,7 +38,7 @@ void encodeEntryCustomMetadata() { // length 3, encoded as length - 1 since 0 is not authorized byte expected = (byte) 2; ByteBuf content = ByteBufUtils.getRandomByteBuf(2); - Entry entry = new CustomTypeEntry("foo", content); + Entry entry = new Entry("foo", (byte) -1, content); final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); entry.encodeInto(metadata, ByteBufAllocator.DEFAULT); @@ -60,7 +58,7 @@ void encodeEntryPassthroughMetadata() { byte expected = (byte) 0b11111000; ByteBuf content = ByteBufUtils.getRandomByteBuf(2); - Entry.UnknownCompressedTypeEntry entry = new Entry.UnknownCompressedTypeEntry((byte) 120, content); + Entry entry = new Entry(null, (byte) 120, content); final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); entry.encodeInto(metadata, ByteBufAllocator.DEFAULT); @@ -114,4 +112,137 @@ void decodeEntryOnDoneBufferReturnsNull() { .as("empty entry") .isNull(); } + + @Test + void decodeCompositeMetadata() { + //metadata 1: well known + WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; + ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); + metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); + + //metadata 2: custom + String mimeType2 = "application/custom"; + ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); + metadata2.writeChar('E'); + metadata2.writeChar('∑'); + metadata2.writeChar('é'); + metadata2.writeBoolean(true); + metadata2.writeChar('W'); + + //metadata 3: reserved but unknown + byte reserved = 120; + assertThat(WellKnownMimeType.fromId(reserved)) + .as("ensure UNKNOWN RESERVED used in test") + .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); + ByteBuf metadata3 = ByteBufAllocator.DEFAULT.buffer(); + metadata3.writeByte(88); + + CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); + CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); + CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, reserved, metadata3); + + //can decode 3 + Entry entry1 = Entry.decodeEntry(compositeMetadata, true); + Entry entry2 = Entry.decodeEntry(compositeMetadata, true); + Entry entry3 = Entry.decodeEntry(compositeMetadata, true); + Entry expectedNoMoreEntries = Entry.decodeEntry(compositeMetadata, true); + + assertThat(expectedNoMoreEntries).isNull(); + assertThat(entry1) + .as("entry1") + .isNotNull() + .satisfies(e -> assertThat(e.getMimeType()) + .as("entry1 mime type") + .isEqualTo(mimeType1.getMime()) + ) + .satisfies(e -> assertThat(e.getMimeId()) + .as("entry1 mime id") + .isEqualTo((byte) mimeType1.getIdentifier()) + ) + .satisfies(e -> assertThat(e.getMetadata().toString(CharsetUtil.UTF_8)) + .as("entry1 decoded") + .isEqualTo("abcdefghijkl") + ); + + assertThat(entry2) + .as("entry2") + .isNotNull() + .satisfies(e -> assertThat(e.getMimeType()) + .as("entry2 mime type") + .isEqualTo(mimeType2) + ) + .satisfies(e -> assertThat(e.getMimeId()) + .as("entry2 mime id") + .isEqualTo((byte) -1) + ) + .satisfies(e -> assertThat(e.getMetadata()) + .as("entry2 decoded") + .isEqualByComparingTo(metadata2) + ); + + assertThat(entry3) + .as("entry3") + .isNotNull() + .satisfies(e -> assertThat(e.getMimeType()) + .as("entry3 mime type") + .isNull() + ) + .satisfies(e -> assertThat(e.getMimeId()) + .as("entry3 mime id") + .isEqualTo(reserved) + ) + .satisfies(e -> assertThat(e.getMetadata()) + .as("entry3 decoded") + .isEqualByComparingTo(metadata3) + ); + } + + @Test + void createCustomTypeEntry() { + Entry entry = Entry.customMime("example/mime", + ByteBufUtils.getRandomByteBuf(5)); + + assertThat(entry.getMimeType()) + .as("mime type") + .isEqualTo("example/mime"); + assertThat(entry.getMimeId()) + .as("mime id") + .isEqualTo((byte) -1); + assertThat(entry.getMetadata().isReadable(5)) + .as("5 bytes content") + .isTrue(); + } + + @Test + void createWellKnownTypeEntry() { + WellKnownMimeType wkn = WellKnownMimeType.APPLICATION_XML; + Entry entry = Entry.wellKnownMime(wkn, ByteBufUtils.getRandomByteBuf(5)); + + assertThat(entry.getMimeType()) + .as("mime type") + .isEqualTo(wkn.getMime()); + assertThat(entry.getMimeId()) + .as("mime id") + .isEqualTo(wkn.getIdentifier()); + assertThat(entry.getMetadata().isReadable(5)) + .as("5 bytes content") + .isTrue(); + } + + @Test + void createCompressedRawTypeEntry() { + byte id = (byte) 120; + Entry entry = Entry.rawCompressedMime(id, ByteBufUtils.getRandomByteBuf(5)); + + assertThat(entry.getMimeType()) + .as("mime type") + .isNull(); + assertThat(entry.getMimeId()) + .as("mime id") + .isEqualTo(id); + assertThat(entry.getMetadata().isReadable(5)) + .as("5 bytes content") + .isTrue(); + } } \ No newline at end of file From dfddaf34e2f5a2bfa835d900561780e5f371ace5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 3 Jun 2019 16:34:28 +0200 Subject: [PATCH 15/25] Rename Entry#decodeEntry to #decode, add #decodeAll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Simon Baslé --- .../metadata/CompositeMetadataFlyweight.java | 35 ++++- .../java/io/rsocket/metadata/EntryTest.java | 132 ++++++++++++++++-- 2 files changed, 153 insertions(+), 14 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java index 498a6e77e..3795542f7 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java @@ -8,6 +8,9 @@ import io.rsocket.util.NumberUtils; import reactor.util.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; + /** * A flyweight class that can be used to encode/decode composite metadata information to/from {@link ByteBuf}. * This is intended for low-level efficient manipulation of such buffers, but each composite metadata entry can be also @@ -335,7 +338,7 @@ public static Entry rawCompressedMime(byte mimeCode, ByteBuf metadataContentBuff * @param retainMetadataSlices should each slide be retained when read from the original buffer? * @return the decoded {@link Entry} */ - static Entry decodeEntry(ByteBuf buffer, boolean retainMetadataSlices) { + static Entry decode(ByteBuf buffer, boolean retainMetadataSlices) { ByteBuf[] entry = decodeMimeAndContentBuffers(buffer, retainMetadataSlices); if (entry == METADATA_MALFORMED) { throw new IllegalArgumentException("composite metadata entry buffer is too short to contain proper entry"); @@ -355,7 +358,7 @@ static Entry decodeEntry(ByteBuf buffer, boolean retainMetadataSlices) { byte id = decodeMimeIdFromMimeBuffer(encodedHeader); WellKnownMimeType wkn = WellKnownMimeType.fromId(id); if (wkn == WellKnownMimeType.UNPARSEABLE_MIME_TYPE) { - //should not happen due to flyweight's decodeEntry own guard + //should not happen due to flyweight decodeMimeAndContentBuffer's own guard throw new IllegalStateException("composite metadata entry parsing failed on compressed mime id " + id); } if (wkn == WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE) { @@ -366,13 +369,39 @@ static Entry decodeEntry(ByteBuf buffer, boolean retainMetadataSlices) { else { CharSequence customMimeCharSequence = decodeMimeTypeFromMimeBuffer(encodedHeader); if (customMimeCharSequence == null) { - //should not happen due to flyweight's decodeEntry own guard + //should not happen due to flyweight decodeMimeAndContentBuffer's own guard throw new IllegalArgumentException("composite metadata entry parsing failed on custom type"); } return new Entry(customMimeCharSequence.toString(), (byte) -1, metadataContent); } } + /** + * Decode all the metadata entries from a {@link ByteBuf} into a {@link List} of {@link Entry}. + * This is only possible on frame types used to initiate interactions, if the SETUP metadata mime type was + * {@link WellKnownMimeType#MESSAGE_RSOCKET_COMPOSITE_METADATA}. + *

+ * Each entry's {@link Entry#getMetadata() content} is a {@link ByteBuf#readSlice(int) slice} of the original + * buffer that can also be {@link ByteBuf#readRetainedSlice(int) retained} if needed. + *

+ * The buffer is assumed to contain just enough bytes to represent one or more entries (mime type compressed or + * not). The decoding stops when the buffer reaches 0 readable bytes, and fails if it contains bytes but not + * enough to correctly decode an entry. + * + * @param buffer the buffer to decode + * @param retainMetadataSlices should each slide be retained when read from the original buffer? + * @return the {@link List} of decoded {@link Entry} + */ + static List decodeAll(ByteBuf buffer, boolean retainMetadataSlices) { + List list = new ArrayList<>(); + Entry nextEntry = decode(buffer, retainMetadataSlices); + while (nextEntry != null) { + list.add(nextEntry); + nextEntry = decode(buffer, retainMetadataSlices); + } + return list; + } + private final String mimeString; private final byte mimeCode; private final ByteBuf content; diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java index ae0b5507a..a22d4e347 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java @@ -9,6 +9,8 @@ import io.rsocket.util.NumberUtils; import org.junit.jupiter.api.Test; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -76,7 +78,7 @@ void decodeEntryTooShortForMimeLength() { fakeEntry.writeByte(120); assertThatIllegalArgumentException() - .isThrownBy(() -> Entry.decodeEntry(fakeEntry, false)) + .isThrownBy(() -> Entry.decode(fakeEntry, false)) .withMessage("composite metadata entry buffer is too short to contain proper entry"); } @@ -87,7 +89,7 @@ void decodeEntryHasNoContentLength() { fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); assertThatIllegalArgumentException() - .isThrownBy(() -> Entry.decodeEntry(fakeEntry, false)) + .isThrownBy(() -> Entry.decode(fakeEntry, false)) .withMessage("composite metadata entry buffer is too short to contain proper entry"); } @@ -100,7 +102,7 @@ void decodeEntryTooShortForContentLength() { fakeEntry.writeChar('w'); assertThatIllegalArgumentException() - .isThrownBy(() -> Entry.decodeEntry(fakeEntry, false)) + .isThrownBy(() -> Entry.decode(fakeEntry, false)) .withMessage("composite metadata entry buffer is too short to contain proper entry"); } @@ -108,13 +110,13 @@ void decodeEntryTooShortForContentLength() { void decodeEntryOnDoneBufferReturnsNull() { ByteBuf fakeBuffer = ByteBufUtils.getRandomByteBuf(0); - assertThat(Entry.decodeEntry(fakeBuffer, false)) + assertThat(Entry.decode(fakeBuffer, false)) .as("empty entry") .isNull(); } @Test - void decodeCompositeMetadata() { + void decodeThreeEntries() { //metadata 1: well known WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); @@ -142,13 +144,14 @@ void decodeCompositeMetadata() { CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, reserved, metadata3); - //can decode 3 - Entry entry1 = Entry.decodeEntry(compositeMetadata, true); - Entry entry2 = Entry.decodeEntry(compositeMetadata, true); - Entry entry3 = Entry.decodeEntry(compositeMetadata, true); - Entry expectedNoMoreEntries = Entry.decodeEntry(compositeMetadata, true); + Entry entry1 = Entry.decode(compositeMetadata, true); + Entry entry2 = Entry.decode(compositeMetadata, true); + Entry entry3 = Entry.decode(compositeMetadata, true); + Entry expectedNoMoreEntries = Entry.decode(compositeMetadata, true); - assertThat(expectedNoMoreEntries).isNull(); + assertThat(expectedNoMoreEntries) + .as("decodes exactly 3") + .isNull(); assertThat(entry1) .as("entry1") .isNotNull() @@ -198,6 +201,113 @@ void decodeCompositeMetadata() { ); } + @Test + void decodeAllEntries() { + //metadata 1: well known + WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; + ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); + metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); + + //metadata 2: custom + String mimeType2 = "application/custom"; + ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); + metadata2.writeChar('E'); + metadata2.writeChar('∑'); + metadata2.writeChar('é'); + metadata2.writeBoolean(true); + metadata2.writeChar('W'); + + //metadata 3: reserved but unknown + byte reserved = 120; + assertThat(WellKnownMimeType.fromId(reserved)) + .as("ensure UNKNOWN RESERVED used in test") + .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); + ByteBuf metadata3 = ByteBufAllocator.DEFAULT.buffer(); + metadata3.writeByte(88); + + CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); + CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); + CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, reserved, metadata3); + + List decoded = Entry.decodeAll(compositeMetadata, true); + + assertThat(decoded) + .as("decodes exactly 3") + .hasSize(3); + + assertThat(decoded.get(0)) + .as("entry1") + .isNotNull() + .satisfies(e -> assertThat(e.getMimeType()) + .as("entry1 mime type") + .isEqualTo(mimeType1.getMime()) + ) + .satisfies(e -> assertThat(e.getMimeId()) + .as("entry1 mime id") + .isEqualTo((byte) mimeType1.getIdentifier()) + ) + .satisfies(e -> assertThat(e.getMetadata().toString(CharsetUtil.UTF_8)) + .as("entry1 decoded") + .isEqualTo("abcdefghijkl") + ); + + assertThat(decoded.get(1)) + .as("entry2") + .isNotNull() + .satisfies(e -> assertThat(e.getMimeType()) + .as("entry2 mime type") + .isEqualTo(mimeType2) + ) + .satisfies(e -> assertThat(e.getMimeId()) + .as("entry2 mime id") + .isEqualTo((byte) -1) + ) + .satisfies(e -> assertThat(e.getMetadata()) + .as("entry2 decoded") + .isEqualByComparingTo(metadata2) + ); + + assertThat(decoded.get(2)) + .as("entry3") + .isNotNull() + .satisfies(e -> assertThat(e.getMimeType()) + .as("entry3 mime type") + .isNull() + ) + .satisfies(e -> assertThat(e.getMimeId()) + .as("entry3 mime id") + .isEqualTo(reserved) + ) + .satisfies(e -> assertThat(e.getMetadata()) + .as("entry3 decoded") + .isEqualByComparingTo(metadata3) + ); + } + + @Test + void decodeAllForEmpty() { + ByteBuf emptyBuffer = ByteBufAllocator.DEFAULT.buffer(0); + assertThat(Entry.decodeAll(emptyBuffer, false)) + .isEmpty(); + } + + @Test + void decodeAllForMalformed() { + CompositeByteBuf compositeByteBuf = ByteBufAllocator.DEFAULT.compositeBuffer(); + //encode a first valid metadata + WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; + ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); + metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); + CompositeMetadataFlyweight.encodeAndAddMetadata(compositeByteBuf, ByteBufAllocator.DEFAULT, mimeType1, metadata1); + //encode an invalid metadata + compositeByteBuf.addComponents(true, ByteBufUtils.getRandomByteBuf(15)); + + assertThatIllegalArgumentException() + .isThrownBy(() -> Entry.decodeAll(compositeByteBuf, false)) + .withMessage("composite metadata entry buffer is too short to contain proper entry"); + } + @Test void createCustomTypeEntry() { Entry entry = Entry.customMime("example/mime", From 1d4093f8dafad9718cff461e109065217d0f0d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 3 Jun 2019 19:16:06 +0200 Subject: [PATCH 16/25] apply Google-style formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Simon Baslé --- .../metadata/CompositeMetadataFlyweight.java | 838 +++++++++-------- .../rsocket/metadata/WellKnownMimeType.java | 224 +++-- .../java/io/rsocket/util/NumberUtils.java | 1 - .../CompositeMetadataFlyweightTest.java | 882 +++++++++--------- .../java/io/rsocket/metadata/EntryTest.java | 631 ++++++------- .../metadata/WellKnownMimeTypeTest.java | 101 +- .../java/io/rsocket/util/NumberUtilsTest.java | 21 +- 7 files changed, 1305 insertions(+), 1393 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java index 3795542f7..299c1c554 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java @@ -6,455 +6,487 @@ import io.netty.buffer.CompositeByteBuf; import io.netty.util.CharsetUtil; import io.rsocket.util.NumberUtils; -import reactor.util.annotation.Nullable; - import java.util.ArrayList; import java.util.List; +import reactor.util.annotation.Nullable; /** - * A flyweight class that can be used to encode/decode composite metadata information to/from {@link ByteBuf}. - * This is intended for low-level efficient manipulation of such buffers, but each composite metadata entry can be also - * manipulated as an higher abstraction {@link Entry} class, which provides its own encoding and decoding primitives. + * A flyweight class that can be used to encode/decode composite metadata information to/from {@link + * ByteBuf}. This is intended for low-level efficient manipulation of such buffers, but each + * composite metadata entry can be also manipulated as an higher abstraction {@link Entry} class, + * which provides its own encoding and decoding primitives. */ public class CompositeMetadataFlyweight { - static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 - static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 - - /** - * Denotes that an attempt at 0-garbage decoding failed because the input buffer - * didn't have enough bytes to represent a complete metadata entry, only part of - * the bytes. - */ - public static final ByteBuf[] METADATA_MALFORMED = new ByteBuf[0]; - /** - * Denotes that an attempt at garbage-free decoding failed because the input buffer - * was completely empty, which generally means that no more entries are present in - * the buffer. - */ - public static final ByteBuf[] METADATA_BUFFERS_DONE = new ByteBuf[0]; - /** - * Denotes that an attempt at higher level decoding of an entry components failed because - * the input buffer was completely empty, which generally means that no more entries are - * present in the buffer. - */ - static final Object[] METADATA_ENTRIES_DONE = new Object[0]; - - private CompositeMetadataFlyweight() {} - - /** - * Decode the next metadata entry (a mime header + content pair of {@link ByteBuf}) from a {@link ByteBuf} that contains - * at least enough bytes for one more such entry. - * The header buffer is either: - *

    - *
  • - * made up of a single byte: this represents an encoded mime id, which can be further decoded using {@link #decodeMimeIdFromMimeBuffer(ByteBuf)} - *
  • - *
  • - * made up of 2 or more bytes: this represents an encoded mime String + its length, which can be further decoded - * using {@link #decodeMimeTypeFromMimeBuffer(ByteBuf)}. Note the encoded length, in the first byte, is skipped - * by this decoding method because the remaining length of the buffer is that of the mime string. - *
  • - *
- * Moreover, if the source buffer is empty of readable bytes it is assumed that the composite has been decoded entirely - * and the {@link #METADATA_BUFFERS_DONE} constant is returned. If the buffer contains some readable bytes but - * not enough for a correct representation of an entry, the {@link #METADATA_MALFORMED} constant is returned. - * - * @param compositeMetadata the source {@link ByteBuf} that originally contains one or more metadata entries - * @param retainSlices should produced metadata entry buffers {@link ByteBuf#slice() slices} be {@link ByteBuf#retainedSlice() retained}? - * @return a {@link ByteBuf} slice array of length 2 containing the mime header buffer and the content buffer, or one - * of the zero-length error constant arrays - */ - public static ByteBuf[] decodeMimeAndContentBuffers(ByteBuf compositeMetadata, boolean retainSlices) { - if (compositeMetadata.isReadable()) { - ByteBuf mime; - int ridx = compositeMetadata.readerIndex(); - byte mimeIdOrLength = compositeMetadata.readByte(); - if ((mimeIdOrLength & STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK) { - mime = retainSlices ? - compositeMetadata.retainedSlice(ridx, 1) : - compositeMetadata.slice(ridx, 1); - } - else { - //M flag unset, remaining 7 bits are the length of the mime - int mimeLength = Byte.toUnsignedInt(mimeIdOrLength) + 1; - - if (compositeMetadata.isReadable(mimeLength)) { //need to be able to read an extra mimeLength bytes - //here we need a way for the returned ByteBuf to differentiate between a - //1-byte length mime type and a 1 byte encoded mime id, preferably without - //re-applying the byte mask. The easiest way is to include the initial byte - //and have further decoding ignore the first byte. 1 byte buffer == id, 2+ byte - //buffer == full mime string. - mime = retainSlices ? - // we accommodate that we don't read from current readerIndex, but - //readerIndex - 1 ("0"), for a total slice size of mimeLength + 1 - compositeMetadata.retainedSlice(ridx, mimeLength + 1) : - compositeMetadata.slice(ridx, mimeLength + 1); - //we thus need to skip the bytes we just sliced, but not the flag/length byte - //which was already skipped in initial read - compositeMetadata.skipBytes(mimeLength); - } - else { - return METADATA_MALFORMED; - } - } - - if (compositeMetadata.isReadable(3)) { - //ensures the length medium can be read - final int metadataLength = compositeMetadata.readUnsignedMedium(); - if (compositeMetadata.isReadable(metadataLength)) { - ByteBuf metadata = retainSlices ? - compositeMetadata.readRetainedSlice(metadataLength) : - compositeMetadata.readSlice(metadataLength); - return new ByteBuf[] { mime, metadata }; - } - else { - return METADATA_MALFORMED; - } - } - else { - return METADATA_MALFORMED; - } + static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 + static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 + + /** + * Denotes that an attempt at 0-garbage decoding failed because the input buffer didn't have + * enough bytes to represent a complete metadata entry, only part of the bytes. + */ + public static final ByteBuf[] METADATA_MALFORMED = new ByteBuf[0]; + /** + * Denotes that an attempt at garbage-free decoding failed because the input buffer was completely + * empty, which generally means that no more entries are present in the buffer. + */ + public static final ByteBuf[] METADATA_BUFFERS_DONE = new ByteBuf[0]; + /** + * Denotes that an attempt at higher level decoding of an entry components failed because the + * input buffer was completely empty, which generally means that no more entries are present in + * the buffer. + */ + static final Object[] METADATA_ENTRIES_DONE = new Object[0]; + + private CompositeMetadataFlyweight() {} + + /** + * Decode the next metadata entry (a mime header + content pair of {@link ByteBuf}) from a {@link + * ByteBuf} that contains at least enough bytes for one more such entry. The header buffer is + * either: + * + *
    + *
  • made up of a single byte: this represents an encoded mime id, which can be further + * decoded using {@link #decodeMimeIdFromMimeBuffer(ByteBuf)} + *
  • made up of 2 or more bytes: this represents an encoded mime String + its length, which + * can be further decoded using {@link #decodeMimeTypeFromMimeBuffer(ByteBuf)}. Note the + * encoded length, in the first byte, is skipped by this decoding method because the + * remaining length of the buffer is that of the mime string. + *
+ * + * Moreover, if the source buffer is empty of readable bytes it is assumed that the composite has + * been decoded entirely and the {@link #METADATA_BUFFERS_DONE} constant is returned. If the + * buffer contains some readable bytes but not enough for a correct representation of an + * entry, the {@link #METADATA_MALFORMED} constant is returned. + * + * @param compositeMetadata the source {@link ByteBuf} that originally contains one or more + * metadata entries + * @param retainSlices should produced metadata entry buffers {@link ByteBuf#slice() slices} be + * {@link ByteBuf#retainedSlice() retained}? + * @return a {@link ByteBuf} slice array of length 2 containing the mime header buffer and the + * content buffer, or one of the zero-length error constant arrays + */ + public static ByteBuf[] decodeMimeAndContentBuffers( + ByteBuf compositeMetadata, boolean retainSlices) { + if (compositeMetadata.isReadable()) { + ByteBuf mime; + int ridx = compositeMetadata.readerIndex(); + byte mimeIdOrLength = compositeMetadata.readByte(); + if ((mimeIdOrLength & STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK) { + mime = + retainSlices + ? compositeMetadata.retainedSlice(ridx, 1) + : compositeMetadata.slice(ridx, 1); + } else { + // M flag unset, remaining 7 bits are the length of the mime + int mimeLength = Byte.toUnsignedInt(mimeIdOrLength) + 1; + + if (compositeMetadata.isReadable( + mimeLength)) { // need to be able to read an extra mimeLength bytes + // here we need a way for the returned ByteBuf to differentiate between a + // 1-byte length mime type and a 1 byte encoded mime id, preferably without + // re-applying the byte mask. The easiest way is to include the initial byte + // and have further decoding ignore the first byte. 1 byte buffer == id, 2+ byte + // buffer == full mime string. + mime = + retainSlices + ? + // we accommodate that we don't read from current readerIndex, but + // readerIndex - 1 ("0"), for a total slice size of mimeLength + 1 + compositeMetadata.retainedSlice(ridx, mimeLength + 1) + : compositeMetadata.slice(ridx, mimeLength + 1); + // we thus need to skip the bytes we just sliced, but not the flag/length byte + // which was already skipped in initial read + compositeMetadata.skipBytes(mimeLength); + } else { + return METADATA_MALFORMED; + } + } + + if (compositeMetadata.isReadable(3)) { + // ensures the length medium can be read + final int metadataLength = compositeMetadata.readUnsignedMedium(); + if (compositeMetadata.isReadable(metadataLength)) { + ByteBuf metadata = + retainSlices + ? compositeMetadata.readRetainedSlice(metadataLength) + : compositeMetadata.readSlice(metadataLength); + return new ByteBuf[] {mime, metadata}; + } else { + return METADATA_MALFORMED; } - return METADATA_BUFFERS_DONE; + } else { + return METADATA_MALFORMED; + } } + return METADATA_BUFFERS_DONE; + } + + /** + * Decode a {@code byte} compressed mime id from a {@link ByteBuf}, assuming said buffer properly + * contains such an id. + * + *

The buffer must have exactly one readable byte, which is assumed to have been tested for + * mime id encoding via the {@link #STREAM_METADATA_KNOWN_MASK} mask ({@code firstByte & + * STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK}). + * + *

If there is no readable byte, the negative identifier of {@link + * WellKnownMimeType#UNPARSEABLE_MIME_TYPE} is returned. + * + * @param mimeBuffer the buffer that should next contain the compressed mime id byte + * @return the compressed mime id, between 0 and 127, or a negative id if the input is invalid + * @see #decodeMimeTypeFromMimeBuffer(ByteBuf) + */ + public static byte decodeMimeIdFromMimeBuffer(ByteBuf mimeBuffer) { + if (mimeBuffer.readableBytes() != 1) { + return WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier(); + } + return (byte) (mimeBuffer.readByte() & STREAM_METADATA_LENGTH_MASK); + } + + /** + * Decode a {@link CharSequence} custome mime type from a {@link ByteBuf}, assuming said buffer + * properly contains such a mime type. + * + *

The buffer must at least have two readable bytes, which distinguishes it from the {@link + * #decodeMimeIdFromMimeBuffer(ByteBuf) compressed id} case. The first byte is a size and the + * remaining bytes must correspond to the {@link CharSequence}, encoded fully in US_ASCII. As a + * result, the first byte can simply be skipped, and the remaining of the buffer be decoded to the + * mime type. + * + *

If the mime header buffer is less than 2 bytes long, returns {@code null}. + * + * @param flyweightMimeBuffer the mime header {@link ByteBuf} that contains length + custom mime + * type + * @return the decoded custom mime type, as a {@link CharSequence}, or null if the input is + * invalid + * @see #decodeMimeIdFromMimeBuffer(ByteBuf) + */ + @Nullable + public static CharSequence decodeMimeTypeFromMimeBuffer(ByteBuf flyweightMimeBuffer) { + if (flyweightMimeBuffer.readableBytes() < 2) { + return null; + } + // the encoded length is assumed to be kept at the start of the buffer + // but also assumed to be irrelevant because the rest of the slice length + // actually already matches _decoded_length + flyweightMimeBuffer.skipBytes(1); + int mimeStringLength = flyweightMimeBuffer.readableBytes(); + return flyweightMimeBuffer.readCharSequence(mimeStringLength, CharsetUtil.US_ASCII); + } + + /** + * Encode a {@link WellKnownMimeType well known mime type} and a metadata value length into a + * newly allocated {@link ByteBuf}. + * + *

This compact representation encodes the mime type via its ID on a single byte, and the + * unsigned value length on 3 additional bytes. + * + * @param allocator the {@link ByteBufAllocator} to use to create the buffer. + * @param mimeType a byte identifier of a {@link WellKnownMimeType} to encode. + * @param metadataLength the metadata length to append to the buffer as an unsigned 24 bits + * integer. + * @return the encoded mime and metadata length information + */ + static ByteBuf encodeMetadataHeader( + ByteBufAllocator allocator, byte mimeType, int metadataLength) { + ByteBuf buffer = allocator.buffer(4, 4).writeByte(mimeType | STREAM_METADATA_KNOWN_MASK); + + NumberUtils.encodeUnsignedMedium(buffer, metadataLength); + + return buffer; + } + + /** + * Encode a custom mime type and a metadata value length into a newly allocated {@link ByteBuf}. + * + *

This larger representation encodes the mime type representation's length on a single byte, + * then the representation itself, then the unsigned metadata value length on 3 additional bytes. + * + * @param allocator the {@link ByteBufAllocator} to use to create the buffer. + * @param customMime a custom mime type to encode. + * @param metadataLength the metadata length to append to the buffer as an unsigned 24 bits + * integer. + * @return the encoded mime and metadata length information + */ + static ByteBuf encodeMetadataHeader( + ByteBufAllocator allocator, String customMime, int metadataLength) { + ByteBuf mimeBuffer = allocator.buffer(customMime.length()); + mimeBuffer.writeCharSequence(customMime, CharsetUtil.UTF_8); + if (!ByteBufUtil.isText(mimeBuffer, CharsetUtil.US_ASCII)) { + throw new IllegalArgumentException("custom mime type must be US_ASCII characters only"); + } + int ml = mimeBuffer.readableBytes(); + if (ml < 1 || ml > 128) { + throw new IllegalArgumentException( + "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + ml--; + + ByteBuf mimeLength = allocator.buffer(1, 1); + mimeLength.writeByte((byte) ml); + + ByteBuf metadataLengthBuffer = allocator.buffer(3, 3); + NumberUtils.encodeUnsignedMedium(metadataLengthBuffer, metadataLength); + + return allocator + .compositeBuffer() + .addComponents(true, mimeLength, mimeBuffer, metadataLengthBuffer); + } + + /** + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf + * buffer}. + * + * @param compositeMetaData the buffer that will hold all composite metadata information. + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param customMimeType the custom mime type to encode. + * @param metadata the metadata value to encode. + */ + // see #encodeMetadataHeader(ByteBufAllocator, String, int) + public static void encodeAndAddMetadata( + CompositeByteBuf compositeMetaData, + ByteBufAllocator allocator, + String customMimeType, + ByteBuf metadata) { + compositeMetaData.addComponents( + true, encodeMetadataHeader(allocator, customMimeType, metadata.readableBytes()), metadata); + } + + /** + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf + * buffer}. + * + * @param compositeMetaData the buffer that will hold all composite metadata information. + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param knownMimeType the {@link WellKnownMimeType} to encode. + * @param metadata the metadata value to encode. + */ + // see #encodeMetadataHeader(ByteBufAllocator, byte, int) + public static void encodeAndAddMetadata( + CompositeByteBuf compositeMetaData, + ByteBufAllocator allocator, + WellKnownMimeType knownMimeType, + ByteBuf metadata) { + compositeMetaData.addComponents( + true, + encodeMetadataHeader(allocator, knownMimeType.getIdentifier(), metadata.readableBytes()), + metadata); + } + + /** + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf + * buffer}. + * + * @param compositeMetaData the buffer that will hold all composite metadata information. + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param unknownCompressedMimeType the id of the {@link + * WellKnownMimeType#UNKNOWN_RESERVED_MIME_TYPE} to encode. + * @param metadata the metadata value to encode. + */ + // see #encodeMetadataHeader(ByteBufAllocator, byte, int) + static void encodeAndAddMetadata( + CompositeByteBuf compositeMetaData, + ByteBufAllocator allocator, + byte unknownCompressedMimeType, + ByteBuf metadata) { + compositeMetaData.addComponents( + true, + encodeMetadataHeader(allocator, unknownCompressedMimeType, metadata.readableBytes()), + metadata); + } + + // === ENTRY === + + /** + * An entry in a Composite Metadata, which exposes the {@link #getMimeType() mime type} and {@link + * ByteBuf} {@link #getMetadata() content} of the metadata entry. + * + *

There is one case where the entry cannot really be used other than by forwarding it to + * another client: when the mime type is represented as a compressed {@code byte} id, but said id + * is only identified as "reserved" in the current implementation ({@link + * WellKnownMimeType#UNKNOWN_RESERVED_MIME_TYPE}). In that case, the corresponding {@link Entry} + * should reflect that by having a {@code null} {@link #getMimeType()} along a positive {@link + * #getMimeId()}. + * + *

Non-null {@link #getMimeType()} along with positive {@link #getMimeId()} denote a compressed + * mime metadata entry, whereas the same with a negative {@link #getMimeId()} would denote a + * custom mime type metadata entry. + * + *

In all three cases, the {@link #getMetadata()} expose the content of the metadata entry as a + * raw {@link ByteBuf}. + */ + public static class Entry { /** - * Decode a {@code byte} compressed mime id from a {@link ByteBuf}, assuming said buffer properly contains such an id. - *

- * The buffer must have exactly one readable byte, which is assumed to have been tested for mime id encoding via - * the {@link #STREAM_METADATA_KNOWN_MASK} mask ({@code firstByte & STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK}). - *

- * If there is no readable byte, the negative identifier of {@link WellKnownMimeType#UNPARSEABLE_MIME_TYPE} is returned. + * Create an {@link Entry} from a {@link WellKnownMimeType}. This will be encoded in a + * compressed format that uses the {@link WellKnownMimeType#getIdentifier() mime identifier}. * - * @param mimeBuffer the buffer that should next contain the compressed mime id byte - * @return the compressed mime id, between 0 and 127, or a negative id if the input is invalid - * @see #decodeMimeTypeFromMimeBuffer(ByteBuf) + * @param mimeType the {@link WellKnownMimeType} to use for the entry + * @param metadataContentBuffer the content {@link ByteBuf} to use for the entry + * @return the new entry */ - public static byte decodeMimeIdFromMimeBuffer(ByteBuf mimeBuffer) { - if (mimeBuffer.readableBytes() != 1) { - return WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier(); - } - return (byte) (mimeBuffer.readByte() & STREAM_METADATA_LENGTH_MASK); + public static Entry wellKnownMime(WellKnownMimeType mimeType, ByteBuf metadataContentBuffer) { + return new Entry(mimeType.getMime(), mimeType.getIdentifier(), metadataContentBuffer); } /** - * Decode a {@link CharSequence} custome mime type from a {@link ByteBuf}, assuming said buffer properly contains such - * a mime type. - *

- * The buffer must at least have two readable bytes, which distinguishes it from the {@link #decodeMimeIdFromMimeBuffer(ByteBuf) compressed id} - * case. The first byte is a size and the remaining bytes must correspond to the {@link CharSequence}, encoded fully - * in US_ASCII. As a result, the first byte can simply be skipped, and the remaining of the buffer be decoded to - * the mime type. - *

- * If the mime header buffer is less than 2 bytes long, returns {@code null}. + * Create an {@link Entry} from a custom mime type represented as an US-ASCII only {@link + * String}. The whole literal mime type will thus be encoded. * - * @param flyweightMimeBuffer the mime header {@link ByteBuf} that contains length + custom mime type - * @return the decoded custom mime type, as a {@link CharSequence}, or null if the input is invalid - * @see #decodeMimeIdFromMimeBuffer(ByteBuf) + * @param mimeType the custom mime type {@link String} + * @param metadataContentBuffer the content {@link ByteBuf} to use for the entry + * @return the new entry */ - @Nullable - public static CharSequence decodeMimeTypeFromMimeBuffer(ByteBuf flyweightMimeBuffer) { - if (flyweightMimeBuffer.readableBytes() < 2) { - return null; - } - //the encoded length is assumed to be kept at the start of the buffer - //but also assumed to be irrelevant because the rest of the slice length - //actually already matches _decoded_length - flyweightMimeBuffer.skipBytes(1); - int mimeStringLength = flyweightMimeBuffer.readableBytes(); - return flyweightMimeBuffer.readCharSequence(mimeStringLength, CharsetUtil.US_ASCII); + public static Entry customMime(String mimeType, ByteBuf metadataContentBuffer) { + return new Entry(mimeType, (byte) -1, metadataContentBuffer); } /** - * Encode a {@link WellKnownMimeType well known mime type} and a metadata value length into a newly allocated - * {@link ByteBuf}. - *

- * This compact representation encodes the mime type via its ID on a single byte, and the unsigned value length on - * 3 additional bytes. + * Create an {@link Entry} from an unrecognized yet valid "well-known" mime type, ie. a {@code + * byte} that would map to {@link WellKnownMimeType#UNKNOWN_RESERVED_MIME_TYPE}. Prefer using + * {@link #wellKnownMime(WellKnownMimeType, ByteBuf)} if the mime code is recognizable by this + * client. + * + *

This case would usually be encountered when decoding a composite metadata entry from a + * remote that uses a more recent version of the {@link WellKnownMimeType} extension, and this + * method can be useful to create an unprocessed entry in such a case, ensuring no information + * is lost when forwarding frames. * - * @param allocator the {@link ByteBufAllocator} to use to create the buffer. - * @param mimeType a byte identifier of a {@link WellKnownMimeType} to encode. - * @param metadataLength the metadata length to append to the buffer as an unsigned 24 bits integer. - * @return the encoded mime and metadata length information + * @param mimeCode the reserved but unrecognized compressed mime type {@code byte} + * @param metadataContentBuffer the content {@link ByteBuf} to use for the entry + * @return the new entry + * @see #wellKnownMime(WellKnownMimeType, ByteBuf) */ - static ByteBuf encodeMetadataHeader(ByteBufAllocator allocator, byte mimeType, int metadataLength) { - ByteBuf buffer = allocator.buffer(4, 4) - .writeByte(mimeType | STREAM_METADATA_KNOWN_MASK); - - NumberUtils.encodeUnsignedMedium(buffer, metadataLength); - - return buffer; + public static Entry rawCompressedMime(byte mimeCode, ByteBuf metadataContentBuffer) { + return new Entry(null, mimeCode, metadataContentBuffer); } /** - * Encode a custom mime type and a metadata value length into a newly allocated {@link ByteBuf}. - *

- * This larger representation encodes the mime type representation's length on a single byte, then the representation - * itself, then the unsigned metadata value length on 3 additional bytes. + * Incrementally decode the next metadata entry from a {@link ByteBuf} into an {@link Entry}. + * This is only possible on frame types used to initiate interactions, if the SETUP metadata + * mime type was {@link WellKnownMimeType#MESSAGE_RSOCKET_COMPOSITE_METADATA}. * - * @param allocator the {@link ByteBufAllocator} to use to create the buffer. - * @param customMime a custom mime type to encode. - * @param metadataLength the metadata length to append to the buffer as an unsigned 24 bits integer. - * @return the encoded mime and metadata length information + *

Each entry {@link ByteBuf} is a {@link ByteBuf#readSlice(int) slice} of the original + * buffer that can also be {@link ByteBuf#readRetainedSlice(int) retained} if needed. + * + * @param buffer the buffer to decode + * @param retainMetadataSlices should each slide be retained when read from the original buffer? + * @return the decoded {@link Entry} */ - static ByteBuf encodeMetadataHeader(ByteBufAllocator allocator, String customMime, int metadataLength) { - ByteBuf mimeBuffer = allocator.buffer(customMime.length()); - mimeBuffer.writeCharSequence(customMime, CharsetUtil.UTF_8); - if (!ByteBufUtil.isText(mimeBuffer, CharsetUtil.US_ASCII)) { - throw new IllegalArgumentException("custom mime type must be US_ASCII characters only"); + static Entry decode(ByteBuf buffer, boolean retainMetadataSlices) { + ByteBuf[] entry = decodeMimeAndContentBuffers(buffer, retainMetadataSlices); + if (entry == METADATA_MALFORMED) { + throw new IllegalArgumentException( + "composite metadata entry buffer is too short to contain proper entry"); + } + if (entry == METADATA_BUFFERS_DONE) { + return null; + } + + ByteBuf encodedHeader = entry[0]; + ByteBuf metadataContent = entry[1]; + + // the flyweight already validated the size of the buffer, + // this is only to distinguish id vs custom type + if (encodedHeader.readableBytes() == 1) { + // id + byte id = decodeMimeIdFromMimeBuffer(encodedHeader); + WellKnownMimeType wkn = WellKnownMimeType.fromId(id); + if (wkn == WellKnownMimeType.UNPARSEABLE_MIME_TYPE) { + // should not happen due to flyweight decodeMimeAndContentBuffer's own guard + throw new IllegalStateException( + "composite metadata entry parsing failed on compressed mime id " + id); } - int ml = mimeBuffer.readableBytes(); - if (ml < 1 || ml > 128) { - throw new IllegalArgumentException("custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + if (wkn == WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE) { + return new Entry(null, id, metadataContent); } - ml--; - - ByteBuf mimeLength = allocator.buffer(1,1); - mimeLength.writeByte((byte) ml); - - ByteBuf metadataLengthBuffer = allocator.buffer(3, 3); - NumberUtils.encodeUnsignedMedium(metadataLengthBuffer, metadataLength); - - return allocator.compositeBuffer() - .addComponents(true, mimeLength, mimeBuffer, metadataLengthBuffer); + return new Entry(wkn.getMime(), wkn.getIdentifier(), metadataContent); + } else { + CharSequence customMimeCharSequence = decodeMimeTypeFromMimeBuffer(encodedHeader); + if (customMimeCharSequence == null) { + // should not happen due to flyweight decodeMimeAndContentBuffer's own guard + throw new IllegalArgumentException( + "composite metadata entry parsing failed on custom type"); + } + return new Entry(customMimeCharSequence.toString(), (byte) -1, metadataContent); + } } /** - * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf buffer}. + * Decode all the metadata entries from a {@link ByteBuf} into a {@link List} of {@link Entry}. + * This is only possible on frame types used to initiate interactions, if the SETUP metadata + * mime type was {@link WellKnownMimeType#MESSAGE_RSOCKET_COMPOSITE_METADATA}. + * + *

Each entry's {@link Entry#getMetadata() content} is a {@link ByteBuf#readSlice(int) slice} + * of the original buffer that can also be {@link ByteBuf#readRetainedSlice(int) retained} if + * needed. * - * @param compositeMetaData the buffer that will hold all composite metadata information. - * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. - * @param customMimeType the custom mime type to encode. - * @param metadata the metadata value to encode. + *

The buffer is assumed to contain just enough bytes to represent one or more entries (mime + * type compressed or not). The decoding stops when the buffer reaches 0 readable bytes, and + * fails if it contains bytes but not enough to correctly decode an entry. + * + * @param buffer the buffer to decode + * @param retainMetadataSlices should each slide be retained when read from the original buffer? + * @return the {@link List} of decoded {@link Entry} */ - //see #encodeMetadataHeader(ByteBufAllocator, String, int) - public static void encodeAndAddMetadata(CompositeByteBuf compositeMetaData, ByteBufAllocator allocator, String customMimeType, ByteBuf metadata) { - compositeMetaData.addComponents(true, - encodeMetadataHeader(allocator, customMimeType, metadata.readableBytes()), - metadata); + static List decodeAll(ByteBuf buffer, boolean retainMetadataSlices) { + List list = new ArrayList<>(); + Entry nextEntry = decode(buffer, retainMetadataSlices); + while (nextEntry != null) { + list.add(nextEntry); + nextEntry = decode(buffer, retainMetadataSlices); + } + return list; } - /** - * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf buffer}. - * - * @param compositeMetaData the buffer that will hold all composite metadata information. - * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. - * @param knownMimeType the {@link WellKnownMimeType} to encode. - * @param metadata the metadata value to encode. - */ - // see #encodeMetadataHeader(ByteBufAllocator, byte, int) - public static void encodeAndAddMetadata(CompositeByteBuf compositeMetaData, ByteBufAllocator allocator, WellKnownMimeType knownMimeType, ByteBuf metadata) { - compositeMetaData.addComponents(true, - encodeMetadataHeader(allocator, knownMimeType.getIdentifier(), - metadata.readableBytes()), metadata); + private final String mimeString; + private final byte mimeCode; + private final ByteBuf content; + + public Entry(@Nullable String mimeString, byte mimeCode, ByteBuf content) { + this.mimeString = mimeString; + this.mimeCode = mimeCode; + this.content = content; } /** - * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf buffer}. + * Returns the mime type {@link String} representation if there is one. + * + *

A {@code null} value should only occur with a positive {@link #getMimeId()}, denoting an + * entry that is compressed but unparseable (see {@link #rawCompressedMime(byte, ByteBuf)}). * - * @param compositeMetaData the buffer that will hold all composite metadata information. - * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. - * @param unknownCompressedMimeType the id of the {@link WellKnownMimeType#UNKNOWN_RESERVED_MIME_TYPE} to encode. - * @param metadata the metadata value to encode. + * @return the mime type for this entry, or null */ - // see #encodeMetadataHeader(ByteBufAllocator, byte, int) - static void encodeAndAddMetadata(CompositeByteBuf compositeMetaData, ByteBufAllocator allocator, byte unknownCompressedMimeType, ByteBuf metadata) { - compositeMetaData.addComponents(true, - encodeMetadataHeader(allocator, unknownCompressedMimeType, - metadata.readableBytes()), metadata); + @Nullable + public String getMimeType() { + return this.mimeString; + } + + /** @return the compressed mime id byte if relevant (0-127), or -1 if not */ + public byte getMimeId() { + return this.mimeCode; } - //=== ENTRY === + /** @return the metadata content of this entry */ + public ByteBuf getMetadata() { + return this.content; + } /** - * An entry in a Composite Metadata, which exposes the {@link #getMimeType() mime type} and {@link ByteBuf} - * {@link #getMetadata() content} of the metadata entry. - *

- * There is one case where the entry cannot really be used other than by forwarding it to another client: when the - * mime type is represented as a compressed {@code byte} id, but said id is only identified as "reserved" in the - * current implementation ({@link WellKnownMimeType#UNKNOWN_RESERVED_MIME_TYPE}). - * In that case, the corresponding {@link Entry} should reflect that by having a {@code null} {@link #getMimeType()} - * along a positive {@link #getMimeId()}. - *

- * Non-null {@link #getMimeType()} along with positive {@link #getMimeId()} denote a compressed mime metadata entry, - * whereas the same with a negative {@link #getMimeId()} would denote a custom mime type metadata entry. - *

- * In all three cases, the {@link #getMetadata()} expose the content of the metadata entry as a raw {@link ByteBuf}. + * Encode this {@link Entry} into a {@link CompositeByteBuf} representing a composite metadata. + * This buffer may already hold components for previous {@link Entry entries}. + * + * @param compositeByteBuf the {@link CompositeByteBuf} to hold the components of the whole + * composite metadata + * @param byteBufAllocator the {@link ByteBufAllocator} to use to allocate new buffers as needed */ - public static class Entry { - - /** - * Create an {@link Entry} from a {@link WellKnownMimeType}. This will be encoded in a compressed format that - * uses the {@link WellKnownMimeType#getIdentifier() mime identifier}. - * - * @param mimeType the {@link WellKnownMimeType} to use for the entry - * @param metadataContentBuffer the content {@link ByteBuf} to use for the entry - * @return the new entry - */ - public static Entry wellKnownMime(WellKnownMimeType mimeType, ByteBuf metadataContentBuffer) { - return new Entry(mimeType.getMime(), mimeType.getIdentifier(), metadataContentBuffer); - } - - /** - * Create an {@link Entry} from a custom mime type represented as an US-ASCII only {@link String}. - * The whole literal mime type will thus be encoded. - * - * @param mimeType the custom mime type {@link String} - * @param metadataContentBuffer the content {@link ByteBuf} to use for the entry - * @return the new entry - */ - public static Entry customMime(String mimeType, ByteBuf metadataContentBuffer) { - return new Entry(mimeType, (byte) -1, metadataContentBuffer); - } - - /** - * Create an {@link Entry} from an unrecognized yet valid "well-known" mime type, ie. a {@code byte} that would map - * to {@link WellKnownMimeType#UNKNOWN_RESERVED_MIME_TYPE}. Prefer using {@link #wellKnownMime(WellKnownMimeType, ByteBuf)} - * if the mime code is recognizable by this client. - *

- * This case would usually be encountered when decoding a composite metadata entry from a remote that uses a more recent - * version of the {@link WellKnownMimeType} extension, and this method can be useful to create an unprocessed entry - * in such a case, ensuring no information is lost when forwarding frames. - * - * @param mimeCode the reserved but unrecognized compressed mime type {@code byte} - * @param metadataContentBuffer the content {@link ByteBuf} to use for the entry - * @return the new entry - * @see #wellKnownMime(WellKnownMimeType, ByteBuf) - */ - public static Entry rawCompressedMime(byte mimeCode, ByteBuf metadataContentBuffer) { - return new Entry(null, mimeCode, metadataContentBuffer); - } - - /** - * Incrementally decode the next metadata entry from a {@link ByteBuf} into an {@link Entry}. - * This is only possible on frame types used to initiate - * interactions, if the SETUP metadata mime type was {@link WellKnownMimeType#MESSAGE_RSOCKET_COMPOSITE_METADATA}. - *

- * Each entry {@link ByteBuf} is a {@link ByteBuf#readSlice(int) slice} of the original buffer that can also be - * {@link ByteBuf#readRetainedSlice(int) retained} if needed. - * - * @param buffer the buffer to decode - * @param retainMetadataSlices should each slide be retained when read from the original buffer? - * @return the decoded {@link Entry} - */ - static Entry decode(ByteBuf buffer, boolean retainMetadataSlices) { - ByteBuf[] entry = decodeMimeAndContentBuffers(buffer, retainMetadataSlices); - if (entry == METADATA_MALFORMED) { - throw new IllegalArgumentException("composite metadata entry buffer is too short to contain proper entry"); - } - if (entry == METADATA_BUFFERS_DONE) { - return null; - } - - ByteBuf encodedHeader = entry[0]; - ByteBuf metadataContent = entry[1]; - - - //the flyweight already validated the size of the buffer, - //this is only to distinguish id vs custom type - if (encodedHeader.readableBytes() == 1) { - //id - byte id = decodeMimeIdFromMimeBuffer(encodedHeader); - WellKnownMimeType wkn = WellKnownMimeType.fromId(id); - if (wkn == WellKnownMimeType.UNPARSEABLE_MIME_TYPE) { - //should not happen due to flyweight decodeMimeAndContentBuffer's own guard - throw new IllegalStateException("composite metadata entry parsing failed on compressed mime id " + id); - } - if (wkn == WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE) { - return new Entry(null, id, metadataContent); - } - return new Entry(wkn.getMime(), wkn.getIdentifier(), metadataContent); - } - else { - CharSequence customMimeCharSequence = decodeMimeTypeFromMimeBuffer(encodedHeader); - if (customMimeCharSequence == null) { - //should not happen due to flyweight decodeMimeAndContentBuffer's own guard - throw new IllegalArgumentException("composite metadata entry parsing failed on custom type"); - } - return new Entry(customMimeCharSequence.toString(), (byte) -1, metadataContent); - } - } - - /** - * Decode all the metadata entries from a {@link ByteBuf} into a {@link List} of {@link Entry}. - * This is only possible on frame types used to initiate interactions, if the SETUP metadata mime type was - * {@link WellKnownMimeType#MESSAGE_RSOCKET_COMPOSITE_METADATA}. - *

- * Each entry's {@link Entry#getMetadata() content} is a {@link ByteBuf#readSlice(int) slice} of the original - * buffer that can also be {@link ByteBuf#readRetainedSlice(int) retained} if needed. - *

- * The buffer is assumed to contain just enough bytes to represent one or more entries (mime type compressed or - * not). The decoding stops when the buffer reaches 0 readable bytes, and fails if it contains bytes but not - * enough to correctly decode an entry. - * - * @param buffer the buffer to decode - * @param retainMetadataSlices should each slide be retained when read from the original buffer? - * @return the {@link List} of decoded {@link Entry} - */ - static List decodeAll(ByteBuf buffer, boolean retainMetadataSlices) { - List list = new ArrayList<>(); - Entry nextEntry = decode(buffer, retainMetadataSlices); - while (nextEntry != null) { - list.add(nextEntry); - nextEntry = decode(buffer, retainMetadataSlices); - } - return list; - } - - private final String mimeString; - private final byte mimeCode; - private final ByteBuf content; - - public Entry(@Nullable String mimeString, byte mimeCode, ByteBuf content) { - this.mimeString = mimeString; - this.mimeCode = mimeCode; - this.content = content; - } - - /** - * Returns the mime type {@link String} representation if there is one. - *

- * A {@code null} value should only occur with a positive {@link #getMimeId()}, - * denoting an entry that is compressed but unparseable (see {@link #rawCompressedMime(byte, ByteBuf)}). - * - * @return the mime type for this entry, or null - */ - @Nullable - public String getMimeType() { - return this.mimeString; - } - - /** - * @return the compressed mime id byte if relevant (0-127), or -1 if not - */ - public byte getMimeId() { - return this.mimeCode; - } - - /** - * @return the metadata content of this entry - */ - public ByteBuf getMetadata() { - return this.content; - } - - /** - * Encode this {@link Entry} into a {@link CompositeByteBuf} representing a composite metadata. - * This buffer may already hold components for previous {@link Entry entries}. - * - * @param compositeByteBuf the {@link CompositeByteBuf} to hold the components of the whole composite metadata - * @param byteBufAllocator the {@link ByteBufAllocator} to use to allocate new buffers as needed - */ - public void encodeInto(CompositeByteBuf compositeByteBuf, ByteBufAllocator byteBufAllocator) { - if (this.mimeCode >= 0) { - encodeAndAddMetadata(compositeByteBuf, byteBufAllocator, - this.mimeCode, this.content); - } - else { - encodeAndAddMetadata(compositeByteBuf, byteBufAllocator, - this.mimeString, this.content); - } - } + public void encodeInto(CompositeByteBuf compositeByteBuf, ByteBufAllocator byteBufAllocator) { + if (this.mimeCode >= 0) { + encodeAndAddMetadata(compositeByteBuf, byteBufAllocator, this.mimeCode, this.content); + } else { + encodeAndAddMetadata(compositeByteBuf, byteBufAllocator, this.mimeString, this.content); + } } + } } diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java index fe685aa5f..0e9c81a61 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java @@ -1,130 +1,128 @@ package io.rsocket.metadata; /** - * Enumeration of Well Known Mime Types, as defined in the eponymous extension. Such mime types - * are used in composite metadata (which can include routing and/or tracing metadata). - * Per specification, identifiers are between 0 and 127 (inclusive). + * Enumeration of Well Known Mime Types, as defined in the eponymous extension. Such mime types are + * used in composite metadata (which can include routing and/or tracing metadata). Per + * specification, identifiers are between 0 and 127 (inclusive). */ public enum WellKnownMimeType { + UNPARSEABLE_MIME_TYPE("UNPARSEABLE_MIME_TYPE_DO_NOT_USE", (byte) -2), + UNKNOWN_RESERVED_MIME_TYPE("UNKNOWN_YET_RESERVED_DO_NOT_USE", (byte) -1), + APPLICATION_AVRO("application/avro", (byte) 0), + APPLICATION_CBOR("application/cbor", (byte) 1), + APPLICATION_GRAPHQL("application/graphql", (byte) 2), + APPLICATION_GZIP("application/gzip", (byte) 3), + APPLICATION_JAVASCRIPT("application/javascript", (byte) 4), + APPLICATION_JSON("application/json", (byte) 5), + APPLICATION_OCTET_STREAM("application/octet-stream", (byte) 6), + APPLICATION_PDF("application/pdf", (byte) 7), + APPLICATION_THRIFT("application/vnd.apache.thrift.binary", (byte) 8), + APPLICATION_PROTOBUF("application/vnd.google.protobuf", (byte) 9), + APPLICATION_XML("application/xml", (byte) 10), + APPLICATION_ZIP("application/zip", (byte) 11), + AUDIO_AAC("audio/aac", (byte) 12), + AUDIO_MP3("audio/mp3", (byte) 13), + AUDIO_MP4("audio/mp4", (byte) 14), + AUDIO_MPEG3("audio/mpeg3", (byte) 15), + AUDIO_MPEG("audio/mpeg", (byte) 16), + AUDIO_OGG("audio/ogg", (byte) 17), + AUDIO_OPUS("audio/opus", (byte) 18), + AUDIO_VORBIS("audio/vorbis", (byte) 19), + IMAGE_BMP("image/bmp", (byte) 20), + IMAGE_GIG("image/gif", (byte) 21), + IMAGE_HEIC_SEQUENCE("image/heic-sequence", (byte) 22), + IMAGE_HEIC("image/heic", (byte) 23), + IMAGE_HEIF_SEQUENCE("image/heif-sequence", (byte) 24), + IMAGE_HEIF("image/heif", (byte) 25), + IMAGE_JPEG("image/jpeg", (byte) 26), + IMAGE_PNG("image/png", (byte) 27), + IMAGE_TIFF("image/tiff", (byte) 28), + MULTIPART_MIXED("multipart/mixed", (byte) 29), + TEXT_CSS("text/css", (byte) 30), + TEXT_CSV("text/csv", (byte) 31), + TEXT_HTML("text/html", (byte) 32), + TEXT_PLAIN("text/plain", (byte) 33), + TEXT_XML("text/xml", (byte) 34), + VIDEO_H264("video/H264", (byte) 35), + VIDEO_H265("video/H265", (byte) 36), + VIDEO_VP8("video/VP8", (byte) 37), + // ... reserved for future use ... + MESSAGE_RSOCKET_TRACING_ZIPKIN("message/x.rsocket.tracing-zipkin.v0", (byte) 125), + MESSAGE_RSOCKET_ROUTING("message/x.rsocket.routing.v0", (byte) 126), + MESSAGE_RSOCKET_COMPOSITE_METADATA("message/x.rsocket.composite-metadata.v0", (byte) 127); - UNPARSEABLE_MIME_TYPE("UNPARSEABLE_MIME_TYPE_DO_NOT_USE", (byte) -2), - UNKNOWN_RESERVED_MIME_TYPE("UNKNOWN_YET_RESERVED_DO_NOT_USE", (byte) -1), - APPLICATION_AVRO("application/avro", (byte)0), - APPLICATION_CBOR("application/cbor", (byte)1), - APPLICATION_GRAPHQL("application/graphql", (byte)2), - APPLICATION_GZIP("application/gzip", (byte)3), - APPLICATION_JAVASCRIPT("application/javascript", (byte)4), - APPLICATION_JSON("application/json", (byte)5), - APPLICATION_OCTET_STREAM("application/octet-stream", (byte)6), - APPLICATION_PDF("application/pdf", (byte)7), - APPLICATION_THRIFT("application/vnd.apache.thrift.binary", (byte) 8), - APPLICATION_PROTOBUF("application/vnd.google.protobuf", (byte)9), - APPLICATION_XML("application/xml", (byte)10), - APPLICATION_ZIP("application/zip", (byte)11), - AUDIO_AAC("audio/aac", (byte)12), - AUDIO_MP3("audio/mp3", (byte)13), - AUDIO_MP4("audio/mp4", (byte)14), - AUDIO_MPEG3("audio/mpeg3", (byte)15), - AUDIO_MPEG("audio/mpeg", (byte)16), - AUDIO_OGG("audio/ogg", (byte)17), - AUDIO_OPUS("audio/opus", (byte)18), - AUDIO_VORBIS("audio/vorbis", (byte)19), - IMAGE_BMP("image/bmp", (byte)20), - IMAGE_GIG("image/gif", (byte)21), - IMAGE_HEIC_SEQUENCE("image/heic-sequence", (byte)22), - IMAGE_HEIC("image/heic", (byte)23), - IMAGE_HEIF_SEQUENCE("image/heif-sequence", (byte)24), - IMAGE_HEIF("image/heif", (byte)25), - IMAGE_JPEG("image/jpeg", (byte)26), - IMAGE_PNG("image/png", (byte)27), - IMAGE_TIFF("image/tiff", (byte)28), - MULTIPART_MIXED("multipart/mixed", (byte)29), - TEXT_CSS("text/css", (byte)30), - TEXT_CSV("text/csv", (byte)31), - TEXT_HTML("text/html", (byte)32), - TEXT_PLAIN("text/plain", (byte)33), - TEXT_XML("text/xml", (byte)34), - VIDEO_H264("video/H264", (byte)35), - VIDEO_H265("video/H265", (byte)36), - VIDEO_VP8("video/VP8", (byte)37), - //... reserved for future use ... - MESSAGE_RSOCKET_TRACING_ZIPKIN("message/x.rsocket.tracing-zipkin.v0", (byte)125), - MESSAGE_RSOCKET_ROUTING("message/x.rsocket.routing.v0", (byte)126), - MESSAGE_RSOCKET_COMPOSITE_METADATA("message/x.rsocket.composite-metadata.v0", (byte)127); + private final String str; + private final byte identifier; - private final String str; - private final byte identifier; + WellKnownMimeType(String str, byte identifier) { + this.str = str; + this.identifier = identifier; + } - WellKnownMimeType(String str, byte identifier) { - this.str = str; - this.identifier = identifier; - } + /** @return the byte identifier of the mime type, guaranteed to be positive or zero. */ + public byte getIdentifier() { + return identifier; + } - /** - * @return the byte identifier of the mime type, guaranteed to be positive or zero. - */ - public byte getIdentifier() { - return identifier; - } + /** + * @return the mime type represented as a {@link String}, which is made of US_ASCII compatible + * characters only + */ + public String getMime() { + return str; + } - /** - * @return the mime type represented as a {@link String}, which is made of US_ASCII compatible characters only - */ - public String getMime() { - return str; - } + /** @see #getMime() */ + @Override + public String toString() { + return str; + } - /** - * @see #getMime() - */ - @Override - public String toString() { - return str; - } + /** + * Find the {@link WellKnownMimeType} for the given {@link String} representation. If the + * representation is {@code null} or doesn't match a {@link WellKnownMimeType}, the {@link + * #UNPARSEABLE_MIME_TYPE} is returned. + * + * @param mimeType the looked up mime type + * @return the matching {@link WellKnownMimeType}, or {@link #UNPARSEABLE_MIME_TYPE} if none + * matches + */ + public static WellKnownMimeType fromMimeType(String mimeType) { + if (mimeType == null) throw new IllegalArgumentException("type must be non-null"); - /** - * Find the {@link WellKnownMimeType} for the given {@link String} representation. - * If the representation is {@code null} or doesn't match a {@link WellKnownMimeType}, the - * {@link #UNPARSEABLE_MIME_TYPE} is returned. - * - * @param mimeType the looked up mime type - * @return the matching {@link WellKnownMimeType}, or {@link #UNPARSEABLE_MIME_TYPE} if none matches - */ - public static WellKnownMimeType fromMimeType(String mimeType) { - if (mimeType == null) throw new IllegalArgumentException("type must be non-null"); - - //force UNPARSEABLE if by chance UNKNOWN_RESERVED_MIME_TYPE's text has been used - if (mimeType.equals(UNKNOWN_RESERVED_MIME_TYPE.str)) { - return UNPARSEABLE_MIME_TYPE; - } + // force UNPARSEABLE if by chance UNKNOWN_RESERVED_MIME_TYPE's text has been used + if (mimeType.equals(UNKNOWN_RESERVED_MIME_TYPE.str)) { + return UNPARSEABLE_MIME_TYPE; + } - for (WellKnownMimeType value : values()) { - if (mimeType.equals(value.str)) { - return value; - } - } - return UNPARSEABLE_MIME_TYPE; + for (WellKnownMimeType value : values()) { + if (mimeType.equals(value.str)) { + return value; + } } + return UNPARSEABLE_MIME_TYPE; + } - /** - * Find the {@link WellKnownMimeType} for the given ID (as an int). Valid IDs are defined to be integers between 0 - * and 127, inclusive. IDs outside of this range will produce the {@link #UNPARSEABLE_MIME_TYPE}. - * Additionally, some IDs in that range are still only reserved and don't have a type associated yet: this - * method returns the {@link #UNKNOWN_RESERVED_MIME_TYPE} when passing such a ID, which lets call sites potentially - * detect this and keep the original representation when transmitting the associated metadata buffer. - * - * @param id the looked up id - * @return the {@link WellKnownMimeType}, or {@link #UNKNOWN_RESERVED_MIME_TYPE} if the id is out of the - * specification's range, or {@link #UNKNOWN_RESERVED_MIME_TYPE} if the id is one that is merely reserved but - * unknown to this implementation. - */ - public static WellKnownMimeType fromId(int id) { - if (id < 0 || id > 127) { - return UNPARSEABLE_MIME_TYPE; - } - for (WellKnownMimeType value : values()) { - if (value.getIdentifier() == id) return value; - } - return UNKNOWN_RESERVED_MIME_TYPE; + /** + * Find the {@link WellKnownMimeType} for the given ID (as an int). Valid IDs are defined to be + * integers between 0 and 127, inclusive. IDs outside of this range will produce the {@link + * #UNPARSEABLE_MIME_TYPE}. Additionally, some IDs in that range are still only reserved and don't + * have a type associated yet: this method returns the {@link #UNKNOWN_RESERVED_MIME_TYPE} when + * passing such a ID, which lets call sites potentially detect this and keep the original + * representation when transmitting the associated metadata buffer. + * + * @param id the looked up id + * @return the {@link WellKnownMimeType}, or {@link #UNKNOWN_RESERVED_MIME_TYPE} if the id is out + * of the specification's range, or {@link #UNKNOWN_RESERVED_MIME_TYPE} if the id is one that + * is merely reserved but unknown to this implementation. + */ + public static WellKnownMimeType fromId(int id) { + if (id < 0 || id > 127) { + return UNPARSEABLE_MIME_TYPE; + } + for (WellKnownMimeType value : values()) { + if (value.getIdentifier() == id) return value; } + return UNKNOWN_RESERVED_MIME_TYPE; + } } diff --git a/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java b/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java index 61a3f3a62..3ff720447 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java +++ b/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java @@ -17,7 +17,6 @@ package io.rsocket.util; import io.netty.buffer.ByteBuf; - import java.util.Objects; public final class NumberUtils { diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java index 7452d7244..54732df74 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java @@ -1,5 +1,9 @@ package io.rsocket.metadata; +import static io.rsocket.metadata.CompositeMetadataFlyweight.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; @@ -8,524 +12,474 @@ import io.rsocket.util.NumberUtils; import org.junit.jupiter.api.Test; -import static io.rsocket.metadata.CompositeMetadataFlyweight.*; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - class CompositeMetadataFlyweightTest { - static String toHeaderBits(ByteBuf encoded) { - encoded.markReaderIndex(); - byte headerByte = encoded.readByte(); - String byteAsString = byteToBitsString(headerByte); - encoded.resetReaderIndex(); - return byteAsString; - } - - static String byteToBitsString(byte b) { - return String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0'); - } - // ==== - - @Test - void knownMimeHeaderZero_avro() { - WellKnownMimeType mime = WellKnownMimeType.APPLICATION_AVRO; - assertThat(mime.getIdentifier()) - .as("smoke test AVRO unsigned 7 bits representation") - .isEqualTo((byte) 0) - .isEqualTo((byte) 0b00000000); - ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); - - assertThat(toHeaderBits(encoded)) - .startsWith("1") - .isEqualTo("10000000") - .isEqualTo(byteToBitsString(mime.getIdentifier()).replaceFirst("0", "1")); - - final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); - assertThat(byteBufs) - .hasSize(2) - .doesNotContainNull(); - - ByteBuf header = byteBufs[0]; - ByteBuf content = byteBufs[1]; - header.markReaderIndex(); - - assertThat(header.readableBytes()) - .as("metadata header size") - .isOne(); - - assertThat(byteToBitsString(header.readByte())) - .as("header bit representation") - .isEqualTo("10000000"); - - header.resetReaderIndex(); - assertThat(decodeMimeIdFromMimeBuffer(header)) - .as("decoded mime id") - .isEqualTo(mime.getIdentifier()); - - assertThat(content.readableBytes()) - .as("no metadata content") - .isZero(); - } - - @Test - void knownMimeHeader127_compositeMetadata() { - WellKnownMimeType mime = WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA; - assertThat(mime.getIdentifier()) - .as("smoke test COMPOSITE unsigned 7 bits representation") - .isEqualTo((byte) 127) - .isEqualTo((byte) 0b01111111); - ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); - - assertThat(toHeaderBits(encoded)) - .startsWith("1") - .isEqualTo("11111111") - .isEqualTo(byteToBitsString(mime.getIdentifier()).replaceFirst("0", "1")); - - final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); - assertThat(byteBufs) - .hasSize(2) - .doesNotContainNull(); - - ByteBuf header = byteBufs[0]; - ByteBuf content = byteBufs[1]; - header.markReaderIndex(); - - assertThat(header.readableBytes()) - .as("metadata header size") - .isOne(); - - assertThat(byteToBitsString(header.readByte())) - .as("header bit representation") - .isEqualTo("11111111"); - - header.resetReaderIndex(); - assertThat(decodeMimeIdFromMimeBuffer(header)) - .as("decoded mime id") - .isEqualTo(mime.getIdentifier()); - - assertThat(content.readableBytes()) - .as("no metadata content") - .isZero(); - } - - @Test - void knownMimeHeader120_reserved() { - byte mime = (byte) 120; - ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime, 0); - - assertThat(mime).as("smoke test RESERVED_120 unsigned 7 bits representation") - .isEqualTo((byte) 0b01111000); - - assertThat(toHeaderBits(encoded)) - .startsWith("1") - .isEqualTo("11111000"); - - final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); - assertThat(byteBufs) - .hasSize(2) - .doesNotContainNull(); - - ByteBuf header = byteBufs[0]; - ByteBuf content = byteBufs[1]; - header.markReaderIndex(); - - assertThat(header.readableBytes()) - .as("metadata header size") - .isOne(); - - assertThat(byteToBitsString(header.readByte())) - .as("header bit representation") - .isEqualTo("11111000"); - - header.resetReaderIndex(); - assertThat(decodeMimeIdFromMimeBuffer(header)) - .as("decoded mime id") - .isEqualTo(mime); + static String toHeaderBits(ByteBuf encoded) { + encoded.markReaderIndex(); + byte headerByte = encoded.readByte(); + String byteAsString = byteToBitsString(headerByte); + encoded.resetReaderIndex(); + return byteAsString; + } + + static String byteToBitsString(byte b) { + return String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0'); + } + // ==== + + @Test + void knownMimeHeaderZero_avro() { + WellKnownMimeType mime = WellKnownMimeType.APPLICATION_AVRO; + assertThat(mime.getIdentifier()) + .as("smoke test AVRO unsigned 7 bits representation") + .isEqualTo((byte) 0) + .isEqualTo((byte) 0b00000000); + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); + + assertThat(toHeaderBits(encoded)) + .startsWith("1") + .isEqualTo("10000000") + .isEqualTo(byteToBitsString(mime.getIdentifier()).replaceFirst("0", "1")); + + final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()).as("metadata header size").isOne(); - assertThat(content.readableBytes()) - .as("no metadata content") - .isZero(); - } + assertThat(byteToBitsString(header.readByte())) + .as("header bit representation") + .isEqualTo("10000000"); - @Test - void customMimeHeaderLengthOne() { - String mimeString ="w"; - ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + header.resetReaderIndex(); + assertThat(decodeMimeIdFromMimeBuffer(header)) + .as("decoded mime id") + .isEqualTo(mime.getIdentifier()); + + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } - assertThat(toHeaderBits(encoded)) - .startsWith("0") - .isEqualTo("00000000"); + @Test + void knownMimeHeader127_compositeMetadata() { + WellKnownMimeType mime = WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA; + assertThat(mime.getIdentifier()) + .as("smoke test COMPOSITE unsigned 7 bits representation") + .isEqualTo((byte) 127) + .isEqualTo((byte) 0b01111111); + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); - final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); - assertThat(byteBufs) - .hasSize(2) - .doesNotContainNull(); + assertThat(toHeaderBits(encoded)) + .startsWith("1") + .isEqualTo("11111111") + .isEqualTo(byteToBitsString(mime.getIdentifier()).replaceFirst("0", "1")); - ByteBuf header = byteBufs[0]; - ByteBuf content = byteBufs[1]; - header.markReaderIndex(); + final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); - assertThat(header.readableBytes()) - .as("metadata header size") - .isGreaterThan(1); + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); - assertThat((int) header.readByte()) - .as("mime length") - .isZero(); //encoded as actual length - 1 + assertThat(header.readableBytes()).as("metadata header size").isOne(); - assertThat(header.readCharSequence(1, CharsetUtil.US_ASCII)) - .as("mime string") - .hasToString(mimeString); + assertThat(byteToBitsString(header.readByte())) + .as("header bit representation") + .isEqualTo("11111111"); - header.resetReaderIndex(); - assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) - .as("decoded mime string") - .hasToString(mimeString); + header.resetReaderIndex(); + assertThat(decodeMimeIdFromMimeBuffer(header)) + .as("decoded mime id") + .isEqualTo(mime.getIdentifier()); - assertThat(content.readableBytes()) - .as("no metadata content") - .isZero(); - } + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } + + @Test + void knownMimeHeader120_reserved() { + byte mime = (byte) 120; + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime, 0); + + assertThat(mime) + .as("smoke test RESERVED_120 unsigned 7 bits representation") + .isEqualTo((byte) 0b01111000); + + assertThat(toHeaderBits(encoded)).startsWith("1").isEqualTo("11111000"); + + final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); - @Test - void customMimeHeaderLengthTwo() { - String mimeString ="ww"; - ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + assertThat(header.readableBytes()).as("metadata header size").isOne(); - assertThat(toHeaderBits(encoded)) - .startsWith("0") - .isEqualTo("00000001"); + assertThat(byteToBitsString(header.readByte())) + .as("header bit representation") + .isEqualTo("11111000"); - final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); - assertThat(byteBufs) - .hasSize(2) - .doesNotContainNull(); + header.resetReaderIndex(); + assertThat(decodeMimeIdFromMimeBuffer(header)).as("decoded mime id").isEqualTo(mime); - ByteBuf header = byteBufs[0]; - ByteBuf content = byteBufs[1]; - header.markReaderIndex(); + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } - assertThat(header.readableBytes()) - .as("metadata header size") - .isGreaterThan(1); + @Test + void customMimeHeaderLengthOne() { + String mimeString = "w"; + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); - assertThat((int) header.readByte()) - .as("mime length") - .isEqualTo(2 - 1); //encoded as actual length - 1 + assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000000"); - assertThat(header.readCharSequence(2, CharsetUtil.US_ASCII)) - .as("mime string") - .hasToString(mimeString); + final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); - header.resetReaderIndex(); - assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) - .as("decoded mime string") - .hasToString(mimeString); + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); - assertThat(content.readableBytes()) - .as("no metadata content") - .isZero(); - } + assertThat(header.readableBytes()).as("metadata header size").isGreaterThan(1); - @Test - void customMimeHeaderLength127() { - StringBuilder builder = new StringBuilder(127); - for (int i = 0; i < 127; i++) { - builder.append('a'); - } - String mimeString = builder.toString(); - ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); - - assertThat(toHeaderBits(encoded)) - .startsWith("0") - .isEqualTo("01111110"); - - final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); - assertThat(byteBufs) - .hasSize(2) - .doesNotContainNull(); - - ByteBuf header = byteBufs[0]; - ByteBuf content = byteBufs[1]; - header.markReaderIndex(); - - assertThat(header.readableBytes()) - .as("metadata header size") - .isGreaterThan(1); - - assertThat((int) header.readByte()) - .as("mime length") - .isEqualTo(127 - 1); //encoded as actual length - 1 - - assertThat(header.readCharSequence(127, CharsetUtil.US_ASCII)) - .as("mime string") - .hasToString(mimeString); - - header.resetReaderIndex(); - assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) - .as("decoded mime string") - .hasToString(mimeString); - - assertThat(content.readableBytes()) - .as("no metadata content") - .isZero(); - } + assertThat((int) header.readByte()).as("mime length").isZero(); // encoded as actual length - 1 - @Test - void customMimeHeaderLength128() { - StringBuilder builder = new StringBuilder(128); - for (int i = 0; i < 128; i++) { - builder.append('a'); - } - String mimeString = builder.toString(); - ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); - - assertThat(toHeaderBits(encoded)) - .startsWith("0") - .isEqualTo("01111111"); - - final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); - assertThat(byteBufs) - .hasSize(2) - .doesNotContainNull(); - - ByteBuf header = byteBufs[0]; - ByteBuf content = byteBufs[1]; - header.markReaderIndex(); - - assertThat(header.readableBytes()) - .as("metadata header size") - .isGreaterThan(1); - - assertThat((int) header.readByte()) - .as("mime length") - .isEqualTo(128 - 1); //encoded as actual length - 1 - - assertThat(header.readCharSequence(128, CharsetUtil.US_ASCII)) - .as("mime string") - .hasToString(mimeString); - - header.resetReaderIndex(); - assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) - .as("decoded mime string") - .hasToString(mimeString); - - assertThat(content.readableBytes()) - .as("no metadata content") - .isZero(); - } + assertThat(header.readCharSequence(1, CharsetUtil.US_ASCII)) + .as("mime string") + .hasToString(mimeString); - @Test - void customMimeHeaderLength129_encodingFails() { - StringBuilder builder = new StringBuilder(129); - for (int i = 0; i < 129; i++) { - builder.append('a'); - } + header.resetReaderIndex(); + assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) + .as("decoded mime string") + .hasToString(mimeString); - assertThatIllegalArgumentException() - .isThrownBy(() -> CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, builder.toString(), 0)) - .withMessage("custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); - } + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } - @Test - void customMimeHeaderNonAscii_encodingFails() { - String mimeNotAscii = "mime/typé"; + @Test + void customMimeHeaderLengthTwo() { + String mimeString = "ww"; + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); - assertThatIllegalArgumentException() - .isThrownBy(() -> CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeNotAscii, 0)) - .withMessage("custom mime type must be US_ASCII characters only"); - } + assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000001"); - @Test - void customMimeHeaderLength0_encodingFails() { - assertThatIllegalArgumentException() - .isThrownBy(() -> CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "", 0)) - .withMessage("custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); - } + final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); - @Test - void decodeEntryTooShortForMimeLength() { - ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); - fakeEntry.writeByte(120); + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); - assertThat(decodeMimeAndContentBuffers(fakeEntry, false)) - .isSameAs(METADATA_MALFORMED); - } + assertThat(header.readableBytes()).as("metadata header size").isGreaterThan(1); - @Test - void decodeEntryHasNoContentLength() { - ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); - fakeEntry.writeByte(0); - fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); - - assertThat(decodeMimeAndContentBuffers(fakeEntry, false)) - .isSameAs(METADATA_MALFORMED); - } + assertThat((int) header.readByte()) + .as("mime length") + .isEqualTo(2 - 1); // encoded as actual length - 1 - @Test - void decodeEntryTooShortForContentLength() { - ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); - fakeEntry.writeByte(1); - fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); - NumberUtils.encodeUnsignedMedium(fakeEntry, 456); - fakeEntry.writeChar('w'); + assertThat(header.readCharSequence(2, CharsetUtil.US_ASCII)) + .as("mime string") + .hasToString(mimeString); - assertThat(decodeMimeAndContentBuffers(fakeEntry, false)) - .isSameAs(METADATA_MALFORMED); - } + header.resetReaderIndex(); + assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) + .as("decoded mime string") + .hasToString(mimeString); - @Test - void decodeEntryAtEndOfBuffer() { - ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } - assertThat(decodeMimeAndContentBuffers(fakeEntry, false)) - .isSameAs(METADATA_BUFFERS_DONE); + @Test + void customMimeHeaderLength127() { + StringBuilder builder = new StringBuilder(127); + for (int i = 0; i < 127; i++) { + builder.append('a'); } + String mimeString = builder.toString(); + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); - @Test - void decodeIdMinusTwoWhenZeroByte() { - ByteBuf fakeIdBuffer = ByteBufAllocator.DEFAULT.buffer(0); - - assertThat(decodeMimeIdFromMimeBuffer(fakeIdBuffer)) - .isEqualTo((WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier())); - } + assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111110"); - @Test - void decodeIdMinusTwoWhenMoreThanOneByte() { - ByteBuf fakeIdBuffer = ByteBufAllocator.DEFAULT.buffer(2); - fakeIdBuffer.writeInt(200); + final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); - assertThat(decodeMimeIdFromMimeBuffer(fakeIdBuffer)) - .isEqualTo((WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier())); - } + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); - @Test - void decodeStringNullIfLengthZero() { - ByteBuf fakeTypeBuffer = ByteBufAllocator.DEFAULT.buffer(2); + assertThat(header.readableBytes()).as("metadata header size").isGreaterThan(1); - assertThat(decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)) - .isNull(); - } + assertThat((int) header.readByte()) + .as("mime length") + .isEqualTo(127 - 1); // encoded as actual length - 1 - @Test - void decodeStringNullIfLengthOne() { - ByteBuf fakeTypeBuffer = ByteBufAllocator.DEFAULT.buffer(2); - fakeTypeBuffer.writeByte(1); + assertThat(header.readCharSequence(127, CharsetUtil.US_ASCII)) + .as("mime string") + .hasToString(mimeString); - assertThat(decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)) - .isNull(); - } + header.resetReaderIndex(); + assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) + .as("decoded mime string") + .hasToString(mimeString); - @Test - void decodeTypeSkipsFirstByte() { - ByteBuf fakeTypeBuffer = ByteBufAllocator.DEFAULT.buffer(2); - fakeTypeBuffer.writeByte(128); - fakeTypeBuffer.writeCharSequence("example", CharsetUtil.US_ASCII); + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } - assertThat(decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)) - .hasToString("example"); + @Test + void customMimeHeaderLength128() { + StringBuilder builder = new StringBuilder(128); + for (int i = 0; i < 128; i++) { + builder.append('a'); } + String mimeString = builder.toString(); + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); - @Test - void encodeMetadataKnownTypeDelegates() { - ByteBuf expected = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, - WellKnownMimeType.APPLICATION_OCTET_STREAM.getIdentifier(), - 2); + assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111111"); - CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); - CompositeMetadataFlyweight.encodeAndAddMetadata( - test, ByteBufAllocator.DEFAULT, - WellKnownMimeType.APPLICATION_OCTET_STREAM, - ByteBufUtils.getRandomByteBuf(2)); + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); - assertThat((Iterable) test) - .hasSize(2) - .first() - .isEqualTo(expected); - } - - @Test - void encodeMetadataReservedTypeDelegates() { - ByteBuf expected = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, - (byte) 120, - 2); - - CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + assertThat(header.readableBytes()).as("metadata header size").isGreaterThan(1); - CompositeMetadataFlyweight.encodeAndAddMetadata( - test, ByteBufAllocator.DEFAULT, - (byte) 120, - ByteBufUtils.getRandomByteBuf(2)); - - assertThat((Iterable) test) - .hasSize(2) - .first() - .isEqualTo(expected); - } + assertThat((int) header.readByte()) + .as("mime length") + .isEqualTo(128 - 1); // encoded as actual length - 1 - @Test - void encodeMetadataCustomTypeDelegates() { - ByteBuf expected = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, - "foo", 2); + assertThat(header.readCharSequence(128, CharsetUtil.US_ASCII)) + .as("mime string") + .hasToString(mimeString); - CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + header.resetReaderIndex(); + assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) + .as("decoded mime string") + .hasToString(mimeString); - CompositeMetadataFlyweight.encodeAndAddMetadata( - test, ByteBufAllocator.DEFAULT, - "foo", - ByteBufUtils.getRandomByteBuf(2)); + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } - assertThat((Iterable) test) - .hasSize(2) - .first() - .isEqualTo(expected); + @Test + void customMimeHeaderLength129_encodingFails() { + StringBuilder builder = new StringBuilder(129); + for (int i = 0; i < 129; i++) { + builder.append('a'); } -// @Test -// void decodeMetadataLengthFromUntouchedWithKnownMime() { -// ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, WellKnownMimeType.APPLICATION_GZIP.getIdentifier(), 12); -// -// assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)) -// .withFailMessage("should not correctly decode if not at correct reader index") -// .isNotEqualTo(12); -// } -// -// @Test -// void decodeMetadataLengthFromMimeDecodedWithKnownMime() { -// ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, WellKnownMimeType.APPLICATION_GZIP.getIdentifier(), 12); -// CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded); -// -// assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)).isEqualTo(12); -// } -// -// @Test -// void decodeMetadataLengthFromUntouchedWithCustomMime() { -// ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo/bar", 12); -// -// assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)) -// .withFailMessage("should not correctly decode if not at correct reader index") -// .isNotEqualTo(12); -// } -// -// @Test -// void decodeMetadataLengthFromMimeDecodedWithCustomMime() { -// ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo/bar", 12); -// CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded); -// -// assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)).isEqualTo(12); -// } -// -// @Test -// void decodeMetadataLengthFromTooShortBuffer() { -// ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); -// buffer.writeShort(12); -// -// assertThatExceptionOfType(RuntimeException.class) -// .isThrownBy(() -> CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(buffer)) -// .withMessage("the given buffer should contain at least 3 readable bytes after decoding mime type"); -// } - - -} \ No newline at end of file + assertThatIllegalArgumentException() + .isThrownBy( + () -> + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, builder.toString(), 0)) + .withMessage( + "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + + @Test + void customMimeHeaderNonAscii_encodingFails() { + String mimeNotAscii = "mime/typé"; + + assertThatIllegalArgumentException() + .isThrownBy( + () -> + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mimeNotAscii, 0)) + .withMessage("custom mime type must be US_ASCII characters only"); + } + + @Test + void customMimeHeaderLength0_encodingFails() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "", 0)) + .withMessage( + "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + + @Test + void decodeEntryTooShortForMimeLength() { + ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + fakeEntry.writeByte(120); + + assertThat(decodeMimeAndContentBuffers(fakeEntry, false)).isSameAs(METADATA_MALFORMED); + } + + @Test + void decodeEntryHasNoContentLength() { + ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + fakeEntry.writeByte(0); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); + + assertThat(decodeMimeAndContentBuffers(fakeEntry, false)).isSameAs(METADATA_MALFORMED); + } + + @Test + void decodeEntryTooShortForContentLength() { + ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + fakeEntry.writeByte(1); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); + NumberUtils.encodeUnsignedMedium(fakeEntry, 456); + fakeEntry.writeChar('w'); + + assertThat(decodeMimeAndContentBuffers(fakeEntry, false)).isSameAs(METADATA_MALFORMED); + } + + @Test + void decodeEntryAtEndOfBuffer() { + ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + + assertThat(decodeMimeAndContentBuffers(fakeEntry, false)).isSameAs(METADATA_BUFFERS_DONE); + } + + @Test + void decodeIdMinusTwoWhenZeroByte() { + ByteBuf fakeIdBuffer = ByteBufAllocator.DEFAULT.buffer(0); + + assertThat(decodeMimeIdFromMimeBuffer(fakeIdBuffer)) + .isEqualTo((WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier())); + } + + @Test + void decodeIdMinusTwoWhenMoreThanOneByte() { + ByteBuf fakeIdBuffer = ByteBufAllocator.DEFAULT.buffer(2); + fakeIdBuffer.writeInt(200); + + assertThat(decodeMimeIdFromMimeBuffer(fakeIdBuffer)) + .isEqualTo((WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier())); + } + + @Test + void decodeStringNullIfLengthZero() { + ByteBuf fakeTypeBuffer = ByteBufAllocator.DEFAULT.buffer(2); + + assertThat(decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)).isNull(); + } + + @Test + void decodeStringNullIfLengthOne() { + ByteBuf fakeTypeBuffer = ByteBufAllocator.DEFAULT.buffer(2); + fakeTypeBuffer.writeByte(1); + + assertThat(decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)).isNull(); + } + + @Test + void decodeTypeSkipsFirstByte() { + ByteBuf fakeTypeBuffer = ByteBufAllocator.DEFAULT.buffer(2); + fakeTypeBuffer.writeByte(128); + fakeTypeBuffer.writeCharSequence("example", CharsetUtil.US_ASCII); + + assertThat(decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)).hasToString("example"); + } + + @Test + void encodeMetadataKnownTypeDelegates() { + ByteBuf expected = + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, + WellKnownMimeType.APPLICATION_OCTET_STREAM.getIdentifier(), + 2); + + CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataFlyweight.encodeAndAddMetadata( + test, + ByteBufAllocator.DEFAULT, + WellKnownMimeType.APPLICATION_OCTET_STREAM, + ByteBufUtils.getRandomByteBuf(2)); + + assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + } + + @Test + void encodeMetadataReservedTypeDelegates() { + ByteBuf expected = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, (byte) 120, 2); + + CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataFlyweight.encodeAndAddMetadata( + test, ByteBufAllocator.DEFAULT, (byte) 120, ByteBufUtils.getRandomByteBuf(2)); + + assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + } + + @Test + void encodeMetadataCustomTypeDelegates() { + ByteBuf expected = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo", 2); + + CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataFlyweight.encodeAndAddMetadata( + test, ByteBufAllocator.DEFAULT, "foo", ByteBufUtils.getRandomByteBuf(2)); + + assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + } + + // @Test + // void decodeMetadataLengthFromUntouchedWithKnownMime() { + // ByteBuf encoded = + // CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, + // WellKnownMimeType.APPLICATION_GZIP.getIdentifier(), 12); + // + // assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)) + // .withFailMessage("should not correctly decode if not at correct reader index") + // .isNotEqualTo(12); + // } + // + // @Test + // void decodeMetadataLengthFromMimeDecodedWithKnownMime() { + // ByteBuf encoded = + // CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, + // WellKnownMimeType.APPLICATION_GZIP.getIdentifier(), 12); + // CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded); + // + // + // assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)).isEqualTo(12); + // } + // + // @Test + // void decodeMetadataLengthFromUntouchedWithCustomMime() { + // ByteBuf encoded = + // CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo/bar", 12); + // + // assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)) + // .withFailMessage("should not correctly decode if not at correct reader index") + // .isNotEqualTo(12); + // } + // + // @Test + // void decodeMetadataLengthFromMimeDecodedWithCustomMime() { + // ByteBuf encoded = + // CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo/bar", 12); + // CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded); + // + // + // assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)).isEqualTo(12); + // } + // + // @Test + // void decodeMetadataLengthFromTooShortBuffer() { + // ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); + // buffer.writeShort(12); + // + // assertThatExceptionOfType(RuntimeException.class) + // .isThrownBy(() -> + // CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(buffer)) + // .withMessage("the given buffer should contain at least 3 readable bytes after + // decoding mime type"); + // } + +} diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java index a22d4e347..53b54fba4 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java @@ -1,5 +1,8 @@ package io.rsocket.metadata; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; @@ -7,352 +10,290 @@ import io.rsocket.metadata.CompositeMetadataFlyweight.Entry; import io.rsocket.test.util.ByteBufUtils; import io.rsocket.util.NumberUtils; -import org.junit.jupiter.api.Test; - import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import org.junit.jupiter.api.Test; class EntryTest { - @Test - void encodeEntryWellKnownMetadata() { - WellKnownMimeType type = WellKnownMimeType.fromId(5); - //5 = 0b00000101 - byte expected = (byte) 0b10000101; - - ByteBuf content = ByteBufUtils.getRandomByteBuf(2); - Entry entry = new Entry(type.getMime(), type.getIdentifier(), content); - - final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - entry.encodeInto(metadata, ByteBufAllocator.DEFAULT); - - assertThat(metadata.readByte()) - .as("mime header") - .isEqualTo(expected); - assertThat(metadata.readUnsignedMedium()).as("length header").isEqualTo(2); - assertThat(metadata.readSlice(2)).as("content").isEqualByComparingTo(content); - } - - @Test - void encodeEntryCustomMetadata() { - // length 3, encoded as length - 1 since 0 is not authorized - byte expected = (byte) 2; - ByteBuf content = ByteBufUtils.getRandomByteBuf(2); - Entry entry = new Entry("foo", (byte) -1, content); - - final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - entry.encodeInto(metadata, ByteBufAllocator.DEFAULT); - - assertThat(metadata.readByte()) - .as("mime header") - .isEqualTo(expected); - assertThat(metadata.readCharSequence(3, CharsetUtil.US_ASCII).toString()) - .isEqualTo("foo"); - assertThat(metadata.readUnsignedMedium()).as("length header").isEqualTo(2); - assertThat(metadata.readSlice(2)).as("content").isEqualByComparingTo(content); - } - - @Test - void encodeEntryPassthroughMetadata() { - //120 = 0b01111000 - byte expected = (byte) 0b11111000; - - ByteBuf content = ByteBufUtils.getRandomByteBuf(2); - Entry entry = new Entry(null, (byte) 120, content); - - final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - entry.encodeInto(metadata, ByteBufAllocator.DEFAULT); - - assertThat(metadata.readByte()) - .as("mime header") - .isEqualTo(expected); - assertThat(metadata.readUnsignedMedium()).as("length header").isEqualTo(2); - assertThat(metadata.readSlice(2)).as("content").isEqualByComparingTo(content); - } - - @Test - void decodeEntryTooShortForMimeLength() { - ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); - fakeEntry.writeByte(120); - - assertThatIllegalArgumentException() - .isThrownBy(() -> Entry.decode(fakeEntry, false)) - .withMessage("composite metadata entry buffer is too short to contain proper entry"); - } - - @Test - void decodeEntryHasNoContentLength() { - ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); - fakeEntry.writeByte(0); - fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); - - assertThatIllegalArgumentException() - .isThrownBy(() -> Entry.decode(fakeEntry, false)) - .withMessage("composite metadata entry buffer is too short to contain proper entry"); - } - - @Test - void decodeEntryTooShortForContentLength() { - ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); - fakeEntry.writeByte(1); - fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); - NumberUtils.encodeUnsignedMedium(fakeEntry, 456); - fakeEntry.writeChar('w'); - - assertThatIllegalArgumentException() - .isThrownBy(() -> Entry.decode(fakeEntry, false)) - .withMessage("composite metadata entry buffer is too short to contain proper entry"); - } - - @Test - void decodeEntryOnDoneBufferReturnsNull() { - ByteBuf fakeBuffer = ByteBufUtils.getRandomByteBuf(0); - - assertThat(Entry.decode(fakeBuffer, false)) - .as("empty entry") - .isNull(); - } - - @Test - void decodeThreeEntries() { - //metadata 1: well known - WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; - ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); - metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); - - //metadata 2: custom - String mimeType2 = "application/custom"; - ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); - metadata2.writeChar('E'); - metadata2.writeChar('∑'); - metadata2.writeChar('é'); - metadata2.writeBoolean(true); - metadata2.writeChar('W'); - - //metadata 3: reserved but unknown - byte reserved = 120; - assertThat(WellKnownMimeType.fromId(reserved)) - .as("ensure UNKNOWN RESERVED used in test") - .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); - ByteBuf metadata3 = ByteBufAllocator.DEFAULT.buffer(); - metadata3.writeByte(88); - - CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); - CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); - CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, reserved, metadata3); - - Entry entry1 = Entry.decode(compositeMetadata, true); - Entry entry2 = Entry.decode(compositeMetadata, true); - Entry entry3 = Entry.decode(compositeMetadata, true); - Entry expectedNoMoreEntries = Entry.decode(compositeMetadata, true); - - assertThat(expectedNoMoreEntries) - .as("decodes exactly 3") - .isNull(); - assertThat(entry1) - .as("entry1") - .isNotNull() - .satisfies(e -> assertThat(e.getMimeType()) - .as("entry1 mime type") - .isEqualTo(mimeType1.getMime()) - ) - .satisfies(e -> assertThat(e.getMimeId()) - .as("entry1 mime id") - .isEqualTo((byte) mimeType1.getIdentifier()) - ) - .satisfies(e -> assertThat(e.getMetadata().toString(CharsetUtil.UTF_8)) - .as("entry1 decoded") - .isEqualTo("abcdefghijkl") - ); - - assertThat(entry2) - .as("entry2") - .isNotNull() - .satisfies(e -> assertThat(e.getMimeType()) - .as("entry2 mime type") - .isEqualTo(mimeType2) - ) - .satisfies(e -> assertThat(e.getMimeId()) - .as("entry2 mime id") - .isEqualTo((byte) -1) - ) - .satisfies(e -> assertThat(e.getMetadata()) - .as("entry2 decoded") - .isEqualByComparingTo(metadata2) - ); - - assertThat(entry3) - .as("entry3") - .isNotNull() - .satisfies(e -> assertThat(e.getMimeType()) - .as("entry3 mime type") - .isNull() - ) - .satisfies(e -> assertThat(e.getMimeId()) - .as("entry3 mime id") - .isEqualTo(reserved) - ) - .satisfies(e -> assertThat(e.getMetadata()) - .as("entry3 decoded") - .isEqualByComparingTo(metadata3) - ); - } - - @Test - void decodeAllEntries() { - //metadata 1: well known - WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; - ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); - metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); - - //metadata 2: custom - String mimeType2 = "application/custom"; - ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); - metadata2.writeChar('E'); - metadata2.writeChar('∑'); - metadata2.writeChar('é'); - metadata2.writeBoolean(true); - metadata2.writeChar('W'); - - //metadata 3: reserved but unknown - byte reserved = 120; - assertThat(WellKnownMimeType.fromId(reserved)) - .as("ensure UNKNOWN RESERVED used in test") - .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); - ByteBuf metadata3 = ByteBufAllocator.DEFAULT.buffer(); - metadata3.writeByte(88); - - CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); - CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); - CompositeMetadataFlyweight.encodeAndAddMetadata(compositeMetadata, ByteBufAllocator.DEFAULT, reserved, metadata3); - - List decoded = Entry.decodeAll(compositeMetadata, true); - - assertThat(decoded) - .as("decodes exactly 3") - .hasSize(3); - - assertThat(decoded.get(0)) - .as("entry1") - .isNotNull() - .satisfies(e -> assertThat(e.getMimeType()) - .as("entry1 mime type") - .isEqualTo(mimeType1.getMime()) - ) - .satisfies(e -> assertThat(e.getMimeId()) - .as("entry1 mime id") - .isEqualTo((byte) mimeType1.getIdentifier()) - ) - .satisfies(e -> assertThat(e.getMetadata().toString(CharsetUtil.UTF_8)) - .as("entry1 decoded") - .isEqualTo("abcdefghijkl") - ); - - assertThat(decoded.get(1)) - .as("entry2") - .isNotNull() - .satisfies(e -> assertThat(e.getMimeType()) - .as("entry2 mime type") - .isEqualTo(mimeType2) - ) - .satisfies(e -> assertThat(e.getMimeId()) - .as("entry2 mime id") - .isEqualTo((byte) -1) - ) - .satisfies(e -> assertThat(e.getMetadata()) - .as("entry2 decoded") - .isEqualByComparingTo(metadata2) - ); - - assertThat(decoded.get(2)) - .as("entry3") - .isNotNull() - .satisfies(e -> assertThat(e.getMimeType()) - .as("entry3 mime type") - .isNull() - ) - .satisfies(e -> assertThat(e.getMimeId()) - .as("entry3 mime id") - .isEqualTo(reserved) - ) - .satisfies(e -> assertThat(e.getMetadata()) - .as("entry3 decoded") - .isEqualByComparingTo(metadata3) - ); - } - - @Test - void decodeAllForEmpty() { - ByteBuf emptyBuffer = ByteBufAllocator.DEFAULT.buffer(0); - assertThat(Entry.decodeAll(emptyBuffer, false)) - .isEmpty(); - } - - @Test - void decodeAllForMalformed() { - CompositeByteBuf compositeByteBuf = ByteBufAllocator.DEFAULT.compositeBuffer(); - //encode a first valid metadata - WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; - ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); - metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); - CompositeMetadataFlyweight.encodeAndAddMetadata(compositeByteBuf, ByteBufAllocator.DEFAULT, mimeType1, metadata1); - //encode an invalid metadata - compositeByteBuf.addComponents(true, ByteBufUtils.getRandomByteBuf(15)); - - assertThatIllegalArgumentException() - .isThrownBy(() -> Entry.decodeAll(compositeByteBuf, false)) - .withMessage("composite metadata entry buffer is too short to contain proper entry"); - } - - @Test - void createCustomTypeEntry() { - Entry entry = Entry.customMime("example/mime", - ByteBufUtils.getRandomByteBuf(5)); - - assertThat(entry.getMimeType()) - .as("mime type") - .isEqualTo("example/mime"); - assertThat(entry.getMimeId()) - .as("mime id") - .isEqualTo((byte) -1); - assertThat(entry.getMetadata().isReadable(5)) - .as("5 bytes content") - .isTrue(); - } - - @Test - void createWellKnownTypeEntry() { - WellKnownMimeType wkn = WellKnownMimeType.APPLICATION_XML; - Entry entry = Entry.wellKnownMime(wkn, ByteBufUtils.getRandomByteBuf(5)); - - assertThat(entry.getMimeType()) - .as("mime type") - .isEqualTo(wkn.getMime()); - assertThat(entry.getMimeId()) - .as("mime id") - .isEqualTo(wkn.getIdentifier()); - assertThat(entry.getMetadata().isReadable(5)) - .as("5 bytes content") - .isTrue(); - } - - @Test - void createCompressedRawTypeEntry() { - byte id = (byte) 120; - Entry entry = Entry.rawCompressedMime(id, ByteBufUtils.getRandomByteBuf(5)); - - assertThat(entry.getMimeType()) - .as("mime type") - .isNull(); - assertThat(entry.getMimeId()) - .as("mime id") - .isEqualTo(id); - assertThat(entry.getMetadata().isReadable(5)) - .as("5 bytes content") - .isTrue(); - } -} \ No newline at end of file + @Test + void encodeEntryWellKnownMetadata() { + WellKnownMimeType type = WellKnownMimeType.fromId(5); + // 5 = 0b00000101 + byte expected = (byte) 0b10000101; + + ByteBuf content = ByteBufUtils.getRandomByteBuf(2); + Entry entry = new Entry(type.getMime(), type.getIdentifier(), content); + + final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + entry.encodeInto(metadata, ByteBufAllocator.DEFAULT); + + assertThat(metadata.readByte()).as("mime header").isEqualTo(expected); + assertThat(metadata.readUnsignedMedium()).as("length header").isEqualTo(2); + assertThat(metadata.readSlice(2)).as("content").isEqualByComparingTo(content); + } + + @Test + void encodeEntryCustomMetadata() { + // length 3, encoded as length - 1 since 0 is not authorized + byte expected = (byte) 2; + ByteBuf content = ByteBufUtils.getRandomByteBuf(2); + Entry entry = new Entry("foo", (byte) -1, content); + + final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + entry.encodeInto(metadata, ByteBufAllocator.DEFAULT); + + assertThat(metadata.readByte()).as("mime header").isEqualTo(expected); + assertThat(metadata.readCharSequence(3, CharsetUtil.US_ASCII).toString()).isEqualTo("foo"); + assertThat(metadata.readUnsignedMedium()).as("length header").isEqualTo(2); + assertThat(metadata.readSlice(2)).as("content").isEqualByComparingTo(content); + } + + @Test + void encodeEntryPassthroughMetadata() { + // 120 = 0b01111000 + byte expected = (byte) 0b11111000; + + ByteBuf content = ByteBufUtils.getRandomByteBuf(2); + Entry entry = new Entry(null, (byte) 120, content); + + final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + entry.encodeInto(metadata, ByteBufAllocator.DEFAULT); + + assertThat(metadata.readByte()).as("mime header").isEqualTo(expected); + assertThat(metadata.readUnsignedMedium()).as("length header").isEqualTo(2); + assertThat(metadata.readSlice(2)).as("content").isEqualByComparingTo(content); + } + + @Test + void decodeEntryTooShortForMimeLength() { + ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + fakeEntry.writeByte(120); + + assertThatIllegalArgumentException() + .isThrownBy(() -> Entry.decode(fakeEntry, false)) + .withMessage("composite metadata entry buffer is too short to contain proper entry"); + } + + @Test + void decodeEntryHasNoContentLength() { + ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + fakeEntry.writeByte(0); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); + + assertThatIllegalArgumentException() + .isThrownBy(() -> Entry.decode(fakeEntry, false)) + .withMessage("composite metadata entry buffer is too short to contain proper entry"); + } + + @Test + void decodeEntryTooShortForContentLength() { + ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + fakeEntry.writeByte(1); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); + NumberUtils.encodeUnsignedMedium(fakeEntry, 456); + fakeEntry.writeChar('w'); + + assertThatIllegalArgumentException() + .isThrownBy(() -> Entry.decode(fakeEntry, false)) + .withMessage("composite metadata entry buffer is too short to contain proper entry"); + } + + @Test + void decodeEntryOnDoneBufferReturnsNull() { + ByteBuf fakeBuffer = ByteBufUtils.getRandomByteBuf(0); + + assertThat(Entry.decode(fakeBuffer, false)).as("empty entry").isNull(); + } + + @Test + void decodeThreeEntries() { + // metadata 1: well known + WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; + ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); + metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); + + // metadata 2: custom + String mimeType2 = "application/custom"; + ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); + metadata2.writeChar('E'); + metadata2.writeChar('∑'); + metadata2.writeChar('é'); + metadata2.writeBoolean(true); + metadata2.writeChar('W'); + + // metadata 3: reserved but unknown + byte reserved = 120; + assertThat(WellKnownMimeType.fromId(reserved)) + .as("ensure UNKNOWN RESERVED used in test") + .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); + ByteBuf metadata3 = ByteBufAllocator.DEFAULT.buffer(); + metadata3.writeByte(88); + + CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeMetadataFlyweight.encodeAndAddMetadata( + compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); + CompositeMetadataFlyweight.encodeAndAddMetadata( + compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); + CompositeMetadataFlyweight.encodeAndAddMetadata( + compositeMetadata, ByteBufAllocator.DEFAULT, reserved, metadata3); + + Entry entry1 = Entry.decode(compositeMetadata, true); + Entry entry2 = Entry.decode(compositeMetadata, true); + Entry entry3 = Entry.decode(compositeMetadata, true); + Entry expectedNoMoreEntries = Entry.decode(compositeMetadata, true); + + assertThat(expectedNoMoreEntries).as("decodes exactly 3").isNull(); + assertThat(entry1) + .as("entry1") + .isNotNull() + .satisfies( + e -> assertThat(e.getMimeType()).as("entry1 mime type").isEqualTo(mimeType1.getMime())) + .satisfies( + e -> + assertThat(e.getMimeId()) + .as("entry1 mime id") + .isEqualTo((byte) mimeType1.getIdentifier())) + .satisfies( + e -> + assertThat(e.getMetadata().toString(CharsetUtil.UTF_8)) + .as("entry1 decoded") + .isEqualTo("abcdefghijkl")); + + assertThat(entry2) + .as("entry2") + .isNotNull() + .satisfies(e -> assertThat(e.getMimeType()).as("entry2 mime type").isEqualTo(mimeType2)) + .satisfies(e -> assertThat(e.getMimeId()).as("entry2 mime id").isEqualTo((byte) -1)) + .satisfies( + e -> assertThat(e.getMetadata()).as("entry2 decoded").isEqualByComparingTo(metadata2)); + + assertThat(entry3) + .as("entry3") + .isNotNull() + .satisfies(e -> assertThat(e.getMimeType()).as("entry3 mime type").isNull()) + .satisfies(e -> assertThat(e.getMimeId()).as("entry3 mime id").isEqualTo(reserved)) + .satisfies( + e -> assertThat(e.getMetadata()).as("entry3 decoded").isEqualByComparingTo(metadata3)); + } + + @Test + void decodeAllEntries() { + // metadata 1: well known + WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; + ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); + metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); + + // metadata 2: custom + String mimeType2 = "application/custom"; + ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); + metadata2.writeChar('E'); + metadata2.writeChar('∑'); + metadata2.writeChar('é'); + metadata2.writeBoolean(true); + metadata2.writeChar('W'); + + // metadata 3: reserved but unknown + byte reserved = 120; + assertThat(WellKnownMimeType.fromId(reserved)) + .as("ensure UNKNOWN RESERVED used in test") + .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); + ByteBuf metadata3 = ByteBufAllocator.DEFAULT.buffer(); + metadata3.writeByte(88); + + CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeMetadataFlyweight.encodeAndAddMetadata( + compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); + CompositeMetadataFlyweight.encodeAndAddMetadata( + compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); + CompositeMetadataFlyweight.encodeAndAddMetadata( + compositeMetadata, ByteBufAllocator.DEFAULT, reserved, metadata3); + + List decoded = Entry.decodeAll(compositeMetadata, true); + + assertThat(decoded).as("decodes exactly 3").hasSize(3); + + assertThat(decoded.get(0)) + .as("entry1") + .isNotNull() + .satisfies( + e -> assertThat(e.getMimeType()).as("entry1 mime type").isEqualTo(mimeType1.getMime())) + .satisfies( + e -> + assertThat(e.getMimeId()) + .as("entry1 mime id") + .isEqualTo((byte) mimeType1.getIdentifier())) + .satisfies( + e -> + assertThat(e.getMetadata().toString(CharsetUtil.UTF_8)) + .as("entry1 decoded") + .isEqualTo("abcdefghijkl")); + + assertThat(decoded.get(1)) + .as("entry2") + .isNotNull() + .satisfies(e -> assertThat(e.getMimeType()).as("entry2 mime type").isEqualTo(mimeType2)) + .satisfies(e -> assertThat(e.getMimeId()).as("entry2 mime id").isEqualTo((byte) -1)) + .satisfies( + e -> assertThat(e.getMetadata()).as("entry2 decoded").isEqualByComparingTo(metadata2)); + + assertThat(decoded.get(2)) + .as("entry3") + .isNotNull() + .satisfies(e -> assertThat(e.getMimeType()).as("entry3 mime type").isNull()) + .satisfies(e -> assertThat(e.getMimeId()).as("entry3 mime id").isEqualTo(reserved)) + .satisfies( + e -> assertThat(e.getMetadata()).as("entry3 decoded").isEqualByComparingTo(metadata3)); + } + + @Test + void decodeAllForEmpty() { + ByteBuf emptyBuffer = ByteBufAllocator.DEFAULT.buffer(0); + assertThat(Entry.decodeAll(emptyBuffer, false)).isEmpty(); + } + + @Test + void decodeAllForMalformed() { + CompositeByteBuf compositeByteBuf = ByteBufAllocator.DEFAULT.compositeBuffer(); + // encode a first valid metadata + WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; + ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); + metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); + CompositeMetadataFlyweight.encodeAndAddMetadata( + compositeByteBuf, ByteBufAllocator.DEFAULT, mimeType1, metadata1); + // encode an invalid metadata + compositeByteBuf.addComponents(true, ByteBufUtils.getRandomByteBuf(15)); + + assertThatIllegalArgumentException() + .isThrownBy(() -> Entry.decodeAll(compositeByteBuf, false)) + .withMessage("composite metadata entry buffer is too short to contain proper entry"); + } + + @Test + void createCustomTypeEntry() { + Entry entry = Entry.customMime("example/mime", ByteBufUtils.getRandomByteBuf(5)); + + assertThat(entry.getMimeType()).as("mime type").isEqualTo("example/mime"); + assertThat(entry.getMimeId()).as("mime id").isEqualTo((byte) -1); + assertThat(entry.getMetadata().isReadable(5)).as("5 bytes content").isTrue(); + } + + @Test + void createWellKnownTypeEntry() { + WellKnownMimeType wkn = WellKnownMimeType.APPLICATION_XML; + Entry entry = Entry.wellKnownMime(wkn, ByteBufUtils.getRandomByteBuf(5)); + + assertThat(entry.getMimeType()).as("mime type").isEqualTo(wkn.getMime()); + assertThat(entry.getMimeId()).as("mime id").isEqualTo(wkn.getIdentifier()); + assertThat(entry.getMetadata().isReadable(5)).as("5 bytes content").isTrue(); + } + + @Test + void createCompressedRawTypeEntry() { + byte id = (byte) 120; + Entry entry = Entry.rawCompressedMime(id, ByteBufUtils.getRandomByteBuf(5)); + + assertThat(entry.getMimeType()).as("mime type").isNull(); + assertThat(entry.getMimeId()).as("mime id").isEqualTo(id); + assertThat(entry.getMetadata().isReadable(5)).as("5 bytes content").isTrue(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java index 885018913..0d36ef671 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java @@ -1,58 +1,57 @@ package io.rsocket.metadata; -import org.junit.jupiter.api.Test; - import static org.assertj.core.api.Assertions.*; -class WellKnownMimeTypeTest { - - @Test - void fromIdMatchFromMimeType() { - for (WellKnownMimeType mimeType : WellKnownMimeType.values()) { - if (mimeType == WellKnownMimeType.UNPARSEABLE_MIME_TYPE - || mimeType == WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE) { - continue; - } - assertThat(WellKnownMimeType.fromMimeType(mimeType.toString())) - .as("mimeType string for " + mimeType.name()) - .isSameAs(mimeType); - - assertThat(WellKnownMimeType.fromId(mimeType.getIdentifier())) - .as("mimeType ID for " + mimeType.name()) - .isSameAs(mimeType); - } - } - - @Test - void fromIdNegative() { - assertThat(WellKnownMimeType.fromId(-1)) - .isSameAs(WellKnownMimeType.fromId(-2)) - .isSameAs(WellKnownMimeType.fromId(-12)) - .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); - } - - @Test - void fromIdGreaterThan127() { - assertThat(WellKnownMimeType.fromId(128)) - .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); - } - - @Test - void fromIdReserved() { - assertThat(WellKnownMimeType.fromId(120)) - .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); - } +import org.junit.jupiter.api.Test; - @Test - void fromMimeTypeUnknown() { - assertThat(WellKnownMimeType.fromMimeType("foo/bar")) - .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); - } +class WellKnownMimeTypeTest { - @Test - void fromMimeTypeUnkwnowReservedStillReturnsUnparseable() { - assertThat(WellKnownMimeType.fromMimeType(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE.getMime())) - .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); + @Test + void fromIdMatchFromMimeType() { + for (WellKnownMimeType mimeType : WellKnownMimeType.values()) { + if (mimeType == WellKnownMimeType.UNPARSEABLE_MIME_TYPE + || mimeType == WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE) { + continue; + } + assertThat(WellKnownMimeType.fromMimeType(mimeType.toString())) + .as("mimeType string for " + mimeType.name()) + .isSameAs(mimeType); + + assertThat(WellKnownMimeType.fromId(mimeType.getIdentifier())) + .as("mimeType ID for " + mimeType.name()) + .isSameAs(mimeType); } - -} \ No newline at end of file + } + + @Test + void fromIdNegative() { + assertThat(WellKnownMimeType.fromId(-1)) + .isSameAs(WellKnownMimeType.fromId(-2)) + .isSameAs(WellKnownMimeType.fromId(-12)) + .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); + } + + @Test + void fromIdGreaterThan127() { + assertThat(WellKnownMimeType.fromId(128)).isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); + } + + @Test + void fromIdReserved() { + assertThat(WellKnownMimeType.fromId(120)) + .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); + } + + @Test + void fromMimeTypeUnknown() { + assertThat(WellKnownMimeType.fromMimeType("foo/bar")) + .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); + } + + @Test + void fromMimeTypeUnkwnowReservedStillReturnsUnparseable() { + assertThat( + WellKnownMimeType.fromMimeType(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE.getMime())) + .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java b/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java index 69e88d86c..46e0f77f4 100644 --- a/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java +++ b/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java @@ -20,8 +20,6 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; -import io.rsocket.test.util.ByteBufUtils; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -166,17 +164,13 @@ void requireUnsignedShortOverFlow() { @Test void encodeUnsignedMedium() { ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); - NumberUtils.encodeUnsignedMedium(buffer,129); + NumberUtils.encodeUnsignedMedium(buffer, 129); buffer.markReaderIndex(); - assertThat(buffer.readUnsignedMedium()) - .as("reading as unsigned medium") - .isEqualTo(129); + assertThat(buffer.readUnsignedMedium()).as("reading as unsigned medium").isEqualTo(129); buffer.resetReaderIndex(); - assertThat(buffer.readMedium()) - .as("reading as signed medium") - .isEqualTo(129); + assertThat(buffer.readMedium()).as("reading as signed medium").isEqualTo(129); } @Test @@ -185,14 +179,9 @@ void encodeUnsignedMediumLarge() { NumberUtils.encodeUnsignedMedium(buffer, 0xFFFFFC); buffer.markReaderIndex(); - assertThat(buffer.readUnsignedMedium()) - .as("reading as unsigned medium") - .isEqualTo(16777212); + assertThat(buffer.readUnsignedMedium()).as("reading as unsigned medium").isEqualTo(16777212); buffer.resetReaderIndex(); - assertThat(buffer.readMedium()) - .as("reading as signed medium") - .isEqualTo(-4); + assertThat(buffer.readMedium()).as("reading as signed medium").isEqualTo(-4); } - } From f822e7e156ee3c6e49fe4f2cdce21994680ea3ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 6 Jun 2019 11:38:37 +0200 Subject: [PATCH 17/25] Use precomputed array for WellKnownMimeType.fromId MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Simon Baslé --- .../rsocket/metadata/WellKnownMimeType.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java index 0e9c81a61..4b7c71314 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java @@ -1,5 +1,7 @@ package io.rsocket.metadata; +import java.util.Arrays; + /** * Enumeration of Well Known Mime Types, as defined in the eponymous extension. Such mime types are * used in composite metadata (which can include routing and/or tracing metadata). Per @@ -51,6 +53,20 @@ public enum WellKnownMimeType { MESSAGE_RSOCKET_ROUTING("message/x.rsocket.routing.v0", (byte) 126), MESSAGE_RSOCKET_COMPOSITE_METADATA("message/x.rsocket.composite-metadata.v0", (byte) 127); + private static final WellKnownMimeType[] TYPES_BY_MIME_ID; + + static { + // precompute an array of all valid mime ids, filling the blanks with the RESERVED enum + TYPES_BY_MIME_ID = new WellKnownMimeType[128]; // 0-127 inclusive + Arrays.fill(TYPES_BY_MIME_ID, UNKNOWN_RESERVED_MIME_TYPE); + + for (WellKnownMimeType value : values()) { + if (value.getIdentifier() >= 0) { + TYPES_BY_MIME_ID[value.getIdentifier()] = value; + } + } + } + private final String str; private final byte identifier; @@ -120,9 +136,6 @@ public static WellKnownMimeType fromId(int id) { if (id < 0 || id > 127) { return UNPARSEABLE_MIME_TYPE; } - for (WellKnownMimeType value : values()) { - if (value.getIdentifier() == id) return value; - } - return UNKNOWN_RESERVED_MIME_TYPE; + return TYPES_BY_MIME_ID[id]; } } From 972d6d1e0edb66f1683a99385b59e1a852f67908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 6 Jun 2019 14:56:24 +0200 Subject: [PATCH 18/25] Use precomputed Map for fromMimeType + jmh benchmark to validate perf increase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Simon Baslé --- .../metadata/WellKnownMimeTypePerf.java | 96 +++++++++++++++++++ .../rsocket/metadata/WellKnownMimeType.java | 15 +-- 2 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 rsocket-core/src/jmh/java/io/rsocket/metadata/WellKnownMimeTypePerf.java diff --git a/rsocket-core/src/jmh/java/io/rsocket/metadata/WellKnownMimeTypePerf.java b/rsocket-core/src/jmh/java/io/rsocket/metadata/WellKnownMimeTypePerf.java new file mode 100644 index 000000000..75598743d --- /dev/null +++ b/rsocket-core/src/jmh/java/io/rsocket/metadata/WellKnownMimeTypePerf.java @@ -0,0 +1,96 @@ +package io.rsocket.metadata; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(Mode.Throughput) +@Fork(value = 1) +@Warmup(iterations = 10) +@Measurement(iterations = 10) +@State(Scope.Thread) +public class WellKnownMimeTypePerf { + + // this is the old values() looping implementation of fromId + private WellKnownMimeType fromIdValuesLoop(int id) { + if (id < 0 || id > 127) { + return WellKnownMimeType.UNPARSEABLE_MIME_TYPE; + } + for (WellKnownMimeType value : WellKnownMimeType.values()) { + if (value.getIdentifier() == id) { + return value; + } + } + return WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE; + } + + // this is the core of the old values() looping implementation of fromMimeType + private WellKnownMimeType fromStringValuesLoop(String mimeType) { + for (WellKnownMimeType value : WellKnownMimeType.values()) { + if (mimeType.equals(value.getMime())) { + return value; + } + } + return WellKnownMimeType.UNPARSEABLE_MIME_TYPE; + } + + @Benchmark + public void fromIdArrayLookup(final Blackhole bh) { + // negative lookup + bh.consume(WellKnownMimeType.fromId(-10)); + bh.consume(WellKnownMimeType.fromId(-1)); + // too large lookup + bh.consume(WellKnownMimeType.fromId(129)); + // first lookup + bh.consume(WellKnownMimeType.fromId(0)); + // middle lookup + bh.consume(WellKnownMimeType.fromId(37)); + // reserved lookup + bh.consume(WellKnownMimeType.fromId(63)); + // last lookup + bh.consume(WellKnownMimeType.fromId(127)); + } + + @Benchmark + public void fromIdValuesLoopLookup(final Blackhole bh) { + // negative lookup + bh.consume(fromIdValuesLoop(-10)); + bh.consume(fromIdValuesLoop(-1)); + // too large lookup + bh.consume(fromIdValuesLoop(129)); + // first lookup + bh.consume(fromIdValuesLoop(0)); + // middle lookup + bh.consume(fromIdValuesLoop(37)); + // reserved lookup + bh.consume(fromIdValuesLoop(63)); + // last lookup + bh.consume(fromIdValuesLoop(127)); + } + + @Benchmark + public void fromStringMapLookup(final Blackhole bh) { + // unknown lookup + bh.consume(WellKnownMimeType.fromMimeType("foo/bar")); + // first lookup + bh.consume(WellKnownMimeType.fromMimeType(WellKnownMimeType.APPLICATION_AVRO.getMime())); + // middle lookup + bh.consume(WellKnownMimeType.fromMimeType(WellKnownMimeType.VIDEO_VP8.getMime())); + // last lookup + bh.consume( + WellKnownMimeType.fromMimeType( + WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getMime())); + } + + @Benchmark + public void fromStringValuesLoopLookup(final Blackhole bh) { + // unknown lookup + bh.consume(fromStringValuesLoop("foo/bar")); + // first lookup + bh.consume(fromStringValuesLoop(WellKnownMimeType.APPLICATION_AVRO.getMime())); + // middle lookup + bh.consume(fromStringValuesLoop(WellKnownMimeType.VIDEO_VP8.getMime())); + // last lookup + bh.consume( + fromStringValuesLoop(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getMime())); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java index 4b7c71314..20a6c741a 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java @@ -1,6 +1,8 @@ package io.rsocket.metadata; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; /** * Enumeration of Well Known Mime Types, as defined in the eponymous extension. Such mime types are @@ -53,16 +55,20 @@ public enum WellKnownMimeType { MESSAGE_RSOCKET_ROUTING("message/x.rsocket.routing.v0", (byte) 126), MESSAGE_RSOCKET_COMPOSITE_METADATA("message/x.rsocket.composite-metadata.v0", (byte) 127); - private static final WellKnownMimeType[] TYPES_BY_MIME_ID; + static final WellKnownMimeType[] TYPES_BY_MIME_ID; + static final Map TYPES_BY_MIME_STRING; static { // precompute an array of all valid mime ids, filling the blanks with the RESERVED enum TYPES_BY_MIME_ID = new WellKnownMimeType[128]; // 0-127 inclusive Arrays.fill(TYPES_BY_MIME_ID, UNKNOWN_RESERVED_MIME_TYPE); + // also prepare a Map of the types by mime string + TYPES_BY_MIME_STRING = new HashMap<>(128); for (WellKnownMimeType value : values()) { if (value.getIdentifier() >= 0) { TYPES_BY_MIME_ID[value.getIdentifier()] = value; + TYPES_BY_MIME_STRING.put(value.getMime(), value); } } } @@ -111,12 +117,7 @@ public static WellKnownMimeType fromMimeType(String mimeType) { return UNPARSEABLE_MIME_TYPE; } - for (WellKnownMimeType value : values()) { - if (mimeType.equals(value.str)) { - return value; - } - } - return UNPARSEABLE_MIME_TYPE; + return TYPES_BY_MIME_STRING.getOrDefault(mimeType, UNPARSEABLE_MIME_TYPE); } /** From 29b10852bb4a59aa46b0a11c6fa11ce4f6326b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 6 Jun 2019 16:52:57 +0200 Subject: [PATCH 19/25] Several custom mime metadata header encoding improvements: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - avoid the use of a CompositeByteBuf - length can be safely predicted due to requirement that custom mime type be ASCII only - still using UTF8 writing and ByteBufUtil.isText(ASCII) to detect non-ascii text as: - writeAscii would not reject non-ascii chars but replace with `?` - ISO-8859 chars (eg. Latin-1) could be considered valid when only checking the number of written bytes - use of ByteBufUtil.writeUtf8 to simplify encoding the mime into the preallocated buffer - release the buffer if there is an encoding exception Signed-off-by: Simon Baslé --- .../metadata/CompositeMetadataFlyweight.java | 35 ++++++++++++------- .../CompositeMetadataFlyweightTest.java | 22 +++++++++--- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java index 299c1c554..1ca1de3f3 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java @@ -211,27 +211,36 @@ static ByteBuf encodeMetadataHeader( */ static ByteBuf encodeMetadataHeader( ByteBufAllocator allocator, String customMime, int metadataLength) { - ByteBuf mimeBuffer = allocator.buffer(customMime.length()); - mimeBuffer.writeCharSequence(customMime, CharsetUtil.UTF_8); - if (!ByteBufUtil.isText(mimeBuffer, CharsetUtil.US_ASCII)) { + ByteBuf metadataHeader = allocator.buffer(4 + customMime.length()); + // reserve 1 byte for the customMime length + int writerIndexInitial = metadataHeader.writerIndex(); + metadataHeader.writerIndex(writerIndexInitial + 1); + + // write the custom mime in UTF8 but validate it is all ASCII-compatible + // (which produces the right result since ASCII chars are still encoded on 1 byte in UTF8) + int customMimeLength = ByteBufUtil.writeUtf8(metadataHeader, customMime); + if (!ByteBufUtil.isText(metadataHeader, CharsetUtil.US_ASCII)) { + metadataHeader.release(); throw new IllegalArgumentException("custom mime type must be US_ASCII characters only"); } - int ml = mimeBuffer.readableBytes(); - if (ml < 1 || ml > 128) { + if (customMimeLength < 1 || customMimeLength > 128) { + metadataHeader.release(); throw new IllegalArgumentException( "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); } - ml--; + metadataHeader.markWriterIndex(); - ByteBuf mimeLength = allocator.buffer(1, 1); - mimeLength.writeByte((byte) ml); + // go back to beginning and write the length + // encoded length is one less than actual length, since 0 is never a valid length, which gives + // wider representation range + metadataHeader.writerIndex(writerIndexInitial); + metadataHeader.writeByte(customMimeLength - 1); - ByteBuf metadataLengthBuffer = allocator.buffer(3, 3); - NumberUtils.encodeUnsignedMedium(metadataLengthBuffer, metadataLength); + // go back to post-mime type and write the metadata content length + metadataHeader.resetWriterIndex(); + NumberUtils.encodeUnsignedMedium(metadataHeader, metadataLength); - return allocator - .compositeBuffer() - .addComponents(true, mimeLength, mimeBuffer, metadataLengthBuffer); + return metadataHeader; } /** diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java index 54732df74..4f1a4a69f 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java @@ -4,9 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.*; import io.netty.util.CharsetUtil; import io.rsocket.test.util.ByteBufUtils; import io.rsocket.util.NumberUtils; @@ -138,6 +136,7 @@ void customMimeHeaderLengthOne() { ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000000"); final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); @@ -169,6 +168,7 @@ void customMimeHeaderLengthTwo() { ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000001"); final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); @@ -206,6 +206,7 @@ void customMimeHeaderLength127() { ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111110"); final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); @@ -243,6 +244,7 @@ void customMimeHeaderLength128() { ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111111"); final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); @@ -287,7 +289,7 @@ void customMimeHeaderLength129_encodingFails() { } @Test - void customMimeHeaderNonAscii_encodingFails() { + void customMimeHeaderLatin1_encodingFails() { String mimeNotAscii = "mime/typé"; assertThatIllegalArgumentException() @@ -298,6 +300,18 @@ void customMimeHeaderNonAscii_encodingFails() { .withMessage("custom mime type must be US_ASCII characters only"); } + @Test + void customMimeHeaderUtf8_encodingFails() { + String mimeNotAscii = + "mime/tyࠒe"; // this is the SAMARITAN LETTER QUF u+0812 represented on 3 bytes + assertThatIllegalArgumentException() + .isThrownBy( + () -> + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mimeNotAscii, 0)) + .withMessage("custom mime type must be US_ASCII characters only"); + } + @Test void customMimeHeaderLength0_encodingFails() { assertThatIllegalArgumentException() From 195a3c37d68902febc564d7a2cccb65597f1b84c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 6 Jun 2019 16:54:42 +0200 Subject: [PATCH 20/25] Use Unpooled buffers in tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Simon Baslé --- .../CompositeMetadataFlyweightTest.java | 20 +++++++++--------- .../java/io/rsocket/metadata/EntryTest.java | 21 ++++++++++--------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java index 4f1a4a69f..ade0bd9f8 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java @@ -323,7 +323,7 @@ void customMimeHeaderLength0_encodingFails() { @Test void decodeEntryTooShortForMimeLength() { - ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + ByteBuf fakeEntry = Unpooled.buffer(); fakeEntry.writeByte(120); assertThat(decodeMimeAndContentBuffers(fakeEntry, false)).isSameAs(METADATA_MALFORMED); @@ -331,7 +331,7 @@ void decodeEntryTooShortForMimeLength() { @Test void decodeEntryHasNoContentLength() { - ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + ByteBuf fakeEntry = Unpooled.buffer(); fakeEntry.writeByte(0); fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); @@ -340,7 +340,7 @@ void decodeEntryHasNoContentLength() { @Test void decodeEntryTooShortForContentLength() { - ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + ByteBuf fakeEntry = Unpooled.buffer(); fakeEntry.writeByte(1); fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); NumberUtils.encodeUnsignedMedium(fakeEntry, 456); @@ -351,14 +351,14 @@ void decodeEntryTooShortForContentLength() { @Test void decodeEntryAtEndOfBuffer() { - ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + ByteBuf fakeEntry = Unpooled.buffer(); assertThat(decodeMimeAndContentBuffers(fakeEntry, false)).isSameAs(METADATA_BUFFERS_DONE); } @Test void decodeIdMinusTwoWhenZeroByte() { - ByteBuf fakeIdBuffer = ByteBufAllocator.DEFAULT.buffer(0); + ByteBuf fakeIdBuffer = Unpooled.buffer(0); assertThat(decodeMimeIdFromMimeBuffer(fakeIdBuffer)) .isEqualTo((WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier())); @@ -366,7 +366,7 @@ void decodeIdMinusTwoWhenZeroByte() { @Test void decodeIdMinusTwoWhenMoreThanOneByte() { - ByteBuf fakeIdBuffer = ByteBufAllocator.DEFAULT.buffer(2); + ByteBuf fakeIdBuffer = Unpooled.buffer(2); fakeIdBuffer.writeInt(200); assertThat(decodeMimeIdFromMimeBuffer(fakeIdBuffer)) @@ -375,14 +375,14 @@ void decodeIdMinusTwoWhenMoreThanOneByte() { @Test void decodeStringNullIfLengthZero() { - ByteBuf fakeTypeBuffer = ByteBufAllocator.DEFAULT.buffer(2); + ByteBuf fakeTypeBuffer = Unpooled.buffer(2); assertThat(decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)).isNull(); } @Test void decodeStringNullIfLengthOne() { - ByteBuf fakeTypeBuffer = ByteBufAllocator.DEFAULT.buffer(2); + ByteBuf fakeTypeBuffer = Unpooled.buffer(2); fakeTypeBuffer.writeByte(1); assertThat(decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)).isNull(); @@ -390,7 +390,7 @@ void decodeStringNullIfLengthOne() { @Test void decodeTypeSkipsFirstByte() { - ByteBuf fakeTypeBuffer = ByteBufAllocator.DEFAULT.buffer(2); + ByteBuf fakeTypeBuffer = Unpooled.buffer(2); fakeTypeBuffer.writeByte(128); fakeTypeBuffer.writeCharSequence("example", CharsetUtil.US_ASCII); @@ -486,7 +486,7 @@ void encodeMetadataCustomTypeDelegates() { // // @Test // void decodeMetadataLengthFromTooShortBuffer() { - // ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); + // ByteBuf buffer = Unpooled.buffer(); // buffer.writeShort(12); // // assertThatExceptionOfType(RuntimeException.class) diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java index 53b54fba4..d0e2d405c 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java @@ -6,6 +6,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; import io.netty.util.CharsetUtil; import io.rsocket.metadata.CompositeMetadataFlyweight.Entry; import io.rsocket.test.util.ByteBufUtils; @@ -66,7 +67,7 @@ void encodeEntryPassthroughMetadata() { @Test void decodeEntryTooShortForMimeLength() { - ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + ByteBuf fakeEntry = Unpooled.buffer(); fakeEntry.writeByte(120); assertThatIllegalArgumentException() @@ -76,7 +77,7 @@ void decodeEntryTooShortForMimeLength() { @Test void decodeEntryHasNoContentLength() { - ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + ByteBuf fakeEntry = Unpooled.buffer(); fakeEntry.writeByte(0); fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); @@ -87,7 +88,7 @@ void decodeEntryHasNoContentLength() { @Test void decodeEntryTooShortForContentLength() { - ByteBuf fakeEntry = ByteBufAllocator.DEFAULT.buffer(); + ByteBuf fakeEntry = Unpooled.buffer(); fakeEntry.writeByte(1); fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); NumberUtils.encodeUnsignedMedium(fakeEntry, 456); @@ -109,12 +110,12 @@ void decodeEntryOnDoneBufferReturnsNull() { void decodeThreeEntries() { // metadata 1: well known WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; - ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); + ByteBuf metadata1 = Unpooled.buffer(); metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); // metadata 2: custom String mimeType2 = "application/custom"; - ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); + ByteBuf metadata2 = Unpooled.buffer(); metadata2.writeChar('E'); metadata2.writeChar('∑'); metadata2.writeChar('é'); @@ -126,7 +127,7 @@ void decodeThreeEntries() { assertThat(WellKnownMimeType.fromId(reserved)) .as("ensure UNKNOWN RESERVED used in test") .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); - ByteBuf metadata3 = ByteBufAllocator.DEFAULT.buffer(); + ByteBuf metadata3 = Unpooled.buffer(); metadata3.writeByte(88); CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); @@ -180,12 +181,12 @@ void decodeThreeEntries() { void decodeAllEntries() { // metadata 1: well known WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; - ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); + ByteBuf metadata1 = Unpooled.buffer(); metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); // metadata 2: custom String mimeType2 = "application/custom"; - ByteBuf metadata2 = ByteBufAllocator.DEFAULT.buffer(); + ByteBuf metadata2 = Unpooled.buffer(); metadata2.writeChar('E'); metadata2.writeChar('∑'); metadata2.writeChar('é'); @@ -197,7 +198,7 @@ void decodeAllEntries() { assertThat(WellKnownMimeType.fromId(reserved)) .as("ensure UNKNOWN RESERVED used in test") .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); - ByteBuf metadata3 = ByteBufAllocator.DEFAULT.buffer(); + ByteBuf metadata3 = Unpooled.buffer(); metadata3.writeByte(88); CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); @@ -256,7 +257,7 @@ void decodeAllForMalformed() { CompositeByteBuf compositeByteBuf = ByteBufAllocator.DEFAULT.compositeBuffer(); // encode a first valid metadata WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; - ByteBuf metadata1 = ByteBufAllocator.DEFAULT.buffer(); + ByteBuf metadata1 = Unpooled.buffer(); metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); CompositeMetadataFlyweight.encodeAndAddMetadata( compositeByteBuf, ByteBufAllocator.DEFAULT, mimeType1, metadata1); From 19d3556253c52e6e1add078be6c26d6ac675e00d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 6 Jun 2019 18:15:18 +0200 Subject: [PATCH 21/25] Remove Entry, replace with CompositeMetadata Iterator-like abstraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CompositeMetadata wraps a ByteBuf and exposes a hasNext/decodeNext API (reminiscent of the Iterator API) as a facade over the flyweight's methods. Each round decodeNext() is correctly invoked, the CompositeMetadata state is updated and the getters can be used to retrieve decoded entry components. Signed-off-by: Simon Baslé --- .../rsocket/metadata/CompositeMetadata.java | 145 +++++++++ .../metadata/CompositeMetadataFlyweight.java | 207 +----------- .../metadata/CompositeMetadataTest.java | 145 +++++++++ .../java/io/rsocket/metadata/EntryTest.java | 300 ------------------ 4 files changed, 292 insertions(+), 505 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java create mode 100644 rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java delete mode 100644 rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java new file mode 100644 index 000000000..7bc733b47 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java @@ -0,0 +1,145 @@ +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import java.util.NoSuchElementException; +import reactor.util.annotation.Nullable; + +/** + * An iterator-like wrapper around a {@link ByteBuf} that exposes metadata entry information at each + * decoding step. This is only possible on frame types used to initiate interactions, if the SETUP + * metadata mime type was {@link WellKnownMimeType#MESSAGE_RSOCKET_COMPOSITE_METADATA}. + * + *

This allows efficient incremental decoding of the entries (which moves the source's {@link + * io.netty.buffer.ByteBuf#readerIndex()}). The buffer is assumed to contain just enough bytes to + * represent one or more entries (mime type compressed or not). The decoding stops when the buffer + * reaches 0 readable bytes ({@code hasNext() == false}), and fails if it contains bytes but not + * enough to correctly decode an entry. + * + *

A note on future-proofness: it is possible to come across a compressed mime type that this + * implementation doesn't recognize. This is likely to be due to the use of a byte id that is merely + * reserved in this implementation, but maps to a {@link WellKnownMimeType} in the implementation + * that encoded the metadata. This can be detected by {@link #getCurrentMimeId()} returning a + * positive {@code byte} while {@link #getCurrentMimeType()} returns {@literal null}. The byte and + * content buffer should be kept around and re-encoded using {@link + * CompositeMetadataFlyweight#encodeAndAddMetadata(CompositeByteBuf, ByteBufAllocator, byte, + * ByteBuf)} in case passing that entry through is required. + */ +public final class CompositeMetadata { + + private final ByteBuf source; + private final boolean retainSlices; + + private byte id; + private @Nullable String mime; + private ByteBuf content; + + /** + * Wrap a composite metadata {@link ByteBuf} to allow incremental decoding of its entries. Each + * decoded {@link ByteBuf} is either a {@link ByteBuf#slice()} or a {@link + * ByteBuf#retainedSlice()} of the original buffer, depending on the {@code retainSlices} + * parameter. + * + * @param fullCompositeMetadataBuffer + */ + public CompositeMetadata(ByteBuf fullCompositeMetadataBuffer, boolean retainSlices) { + this.source = fullCompositeMetadataBuffer; + this.retainSlices = retainSlices; + } + + /** + * Wrap a composite metadata {@link ByteBuf} to allow incremental decoding of its entries. Each + * decoded {@link ByteBuf} is a {@link ByteBuf#retainedSlice()} of the original buffer. + * + * @param fullCompositeMetadataBuffer + */ + public CompositeMetadata(ByteBuf fullCompositeMetadataBuffer) { + this(fullCompositeMetadataBuffer, true); + } + + /** + * Return true if the source buffer still has readable bytes, which is assumed to mean at least + * one more decodable entry. + * + * @return true if the source buffer still has readable bytes + */ + public boolean hasNext() { + return source.isReadable(); + } + + private void reset() { + this.id = -1; + this.mime = null; + this.content = null; + } + + /** + * Decode the next entry in the source buffer, making its values accessible through the {@link + * #getCurrentMimeType()}, {@link #getCurrentMimeId()} and {@link #getCurrentContent()} accessors. + * + * @throws IllegalArgumentException if the buffer contains more data but that data cannot be + * decoded as an entry + * @throws NoSuchElementException if the buffer contains no more data (which can be avoided by + * checking {@link #hasNext()}) + */ + public void decodeNext() { + reset(); + ByteBuf[] decoded = + CompositeMetadataFlyweight.decodeMimeAndContentBuffers(source, retainSlices); + if (decoded == CompositeMetadataFlyweight.METADATA_MALFORMED) { + throw new IllegalArgumentException( + "composite metadata entry buffer is too short to contain proper entry"); + } + if (decoded == CompositeMetadataFlyweight.METADATA_BUFFERS_DONE) { + throw new NoSuchElementException("composite metadata has no more entries"); + } + + ByteBuf header = decoded[0]; + this.content = decoded[1]; + + if (header.readableBytes() == 1) { + this.id = CompositeMetadataFlyweight.decodeMimeIdFromMimeBuffer(header); + WellKnownMimeType wkn = WellKnownMimeType.fromId(id); + if (wkn != WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE) { + this.mime = wkn.getMime(); + } + } else { + this.id = -1; + CharSequence charSequence = CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header); + if (charSequence == null) { + throw new IllegalArgumentException( + "composite metadata entry parsing failed on custom type"); + } + this.mime = charSequence.toString(); + } + } + + /** + * Returns the mime type {@link String} representation of the currently decoded entry, if there is + * one. + * + *

A {@code null} value should only occur with a positive {@link #getCurrentMimeId()}, denoting + * an entry that is compressed but unparseable (probably a buffer encoded by another version of + * the well known mime type extension spec, which is only reserved in this implementation). + * + * @return the mime type for this entry, or null + */ + @Nullable + public String getCurrentMimeType() { + return this.mime; + } + + /** + * @return the compressed mime id byte if relevant (0-127), or -1 if not (custom mime type as + * {@link String}) + */ + public byte getCurrentMimeId() { + return this.id; + } + + /** @return the metadata content of the currently decoded entry */ + public ByteBuf getCurrentContent() { + return this.content; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java index 1ca1de3f3..dc741a723 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java @@ -6,15 +6,12 @@ import io.netty.buffer.CompositeByteBuf; import io.netty.util.CharsetUtil; import io.rsocket.util.NumberUtils; -import java.util.ArrayList; -import java.util.List; import reactor.util.annotation.Nullable; /** * A flyweight class that can be used to encode/decode composite metadata information to/from {@link - * ByteBuf}. This is intended for low-level efficient manipulation of such buffers, but each - * composite metadata entry can be also manipulated as an higher abstraction {@link Entry} class, - * which provides its own encoding and decoding primitives. + * ByteBuf}. This is intended for low-level efficient manipulation of such buffers. See {@link + * CompositeMetadata} for an Iterator-like approach to decoding entries. */ public class CompositeMetadataFlyweight { @@ -31,12 +28,6 @@ public class CompositeMetadataFlyweight { * empty, which generally means that no more entries are present in the buffer. */ public static final ByteBuf[] METADATA_BUFFERS_DONE = new ByteBuf[0]; - /** - * Denotes that an attempt at higher level decoding of an entry components failed because the - * input buffer was completely empty, which generally means that no more entries are present in - * the buffer. - */ - static final Object[] METADATA_ENTRIES_DONE = new Object[0]; private CompositeMetadataFlyweight() {} @@ -304,198 +295,4 @@ static void encodeAndAddMetadata( encodeMetadataHeader(allocator, unknownCompressedMimeType, metadata.readableBytes()), metadata); } - - // === ENTRY === - - /** - * An entry in a Composite Metadata, which exposes the {@link #getMimeType() mime type} and {@link - * ByteBuf} {@link #getMetadata() content} of the metadata entry. - * - *

There is one case where the entry cannot really be used other than by forwarding it to - * another client: when the mime type is represented as a compressed {@code byte} id, but said id - * is only identified as "reserved" in the current implementation ({@link - * WellKnownMimeType#UNKNOWN_RESERVED_MIME_TYPE}). In that case, the corresponding {@link Entry} - * should reflect that by having a {@code null} {@link #getMimeType()} along a positive {@link - * #getMimeId()}. - * - *

Non-null {@link #getMimeType()} along with positive {@link #getMimeId()} denote a compressed - * mime metadata entry, whereas the same with a negative {@link #getMimeId()} would denote a - * custom mime type metadata entry. - * - *

In all three cases, the {@link #getMetadata()} expose the content of the metadata entry as a - * raw {@link ByteBuf}. - */ - public static class Entry { - - /** - * Create an {@link Entry} from a {@link WellKnownMimeType}. This will be encoded in a - * compressed format that uses the {@link WellKnownMimeType#getIdentifier() mime identifier}. - * - * @param mimeType the {@link WellKnownMimeType} to use for the entry - * @param metadataContentBuffer the content {@link ByteBuf} to use for the entry - * @return the new entry - */ - public static Entry wellKnownMime(WellKnownMimeType mimeType, ByteBuf metadataContentBuffer) { - return new Entry(mimeType.getMime(), mimeType.getIdentifier(), metadataContentBuffer); - } - - /** - * Create an {@link Entry} from a custom mime type represented as an US-ASCII only {@link - * String}. The whole literal mime type will thus be encoded. - * - * @param mimeType the custom mime type {@link String} - * @param metadataContentBuffer the content {@link ByteBuf} to use for the entry - * @return the new entry - */ - public static Entry customMime(String mimeType, ByteBuf metadataContentBuffer) { - return new Entry(mimeType, (byte) -1, metadataContentBuffer); - } - - /** - * Create an {@link Entry} from an unrecognized yet valid "well-known" mime type, ie. a {@code - * byte} that would map to {@link WellKnownMimeType#UNKNOWN_RESERVED_MIME_TYPE}. Prefer using - * {@link #wellKnownMime(WellKnownMimeType, ByteBuf)} if the mime code is recognizable by this - * client. - * - *

This case would usually be encountered when decoding a composite metadata entry from a - * remote that uses a more recent version of the {@link WellKnownMimeType} extension, and this - * method can be useful to create an unprocessed entry in such a case, ensuring no information - * is lost when forwarding frames. - * - * @param mimeCode the reserved but unrecognized compressed mime type {@code byte} - * @param metadataContentBuffer the content {@link ByteBuf} to use for the entry - * @return the new entry - * @see #wellKnownMime(WellKnownMimeType, ByteBuf) - */ - public static Entry rawCompressedMime(byte mimeCode, ByteBuf metadataContentBuffer) { - return new Entry(null, mimeCode, metadataContentBuffer); - } - - /** - * Incrementally decode the next metadata entry from a {@link ByteBuf} into an {@link Entry}. - * This is only possible on frame types used to initiate interactions, if the SETUP metadata - * mime type was {@link WellKnownMimeType#MESSAGE_RSOCKET_COMPOSITE_METADATA}. - * - *

Each entry {@link ByteBuf} is a {@link ByteBuf#readSlice(int) slice} of the original - * buffer that can also be {@link ByteBuf#readRetainedSlice(int) retained} if needed. - * - * @param buffer the buffer to decode - * @param retainMetadataSlices should each slide be retained when read from the original buffer? - * @return the decoded {@link Entry} - */ - static Entry decode(ByteBuf buffer, boolean retainMetadataSlices) { - ByteBuf[] entry = decodeMimeAndContentBuffers(buffer, retainMetadataSlices); - if (entry == METADATA_MALFORMED) { - throw new IllegalArgumentException( - "composite metadata entry buffer is too short to contain proper entry"); - } - if (entry == METADATA_BUFFERS_DONE) { - return null; - } - - ByteBuf encodedHeader = entry[0]; - ByteBuf metadataContent = entry[1]; - - // the flyweight already validated the size of the buffer, - // this is only to distinguish id vs custom type - if (encodedHeader.readableBytes() == 1) { - // id - byte id = decodeMimeIdFromMimeBuffer(encodedHeader); - WellKnownMimeType wkn = WellKnownMimeType.fromId(id); - if (wkn == WellKnownMimeType.UNPARSEABLE_MIME_TYPE) { - // should not happen due to flyweight decodeMimeAndContentBuffer's own guard - throw new IllegalStateException( - "composite metadata entry parsing failed on compressed mime id " + id); - } - if (wkn == WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE) { - return new Entry(null, id, metadataContent); - } - return new Entry(wkn.getMime(), wkn.getIdentifier(), metadataContent); - } else { - CharSequence customMimeCharSequence = decodeMimeTypeFromMimeBuffer(encodedHeader); - if (customMimeCharSequence == null) { - // should not happen due to flyweight decodeMimeAndContentBuffer's own guard - throw new IllegalArgumentException( - "composite metadata entry parsing failed on custom type"); - } - return new Entry(customMimeCharSequence.toString(), (byte) -1, metadataContent); - } - } - - /** - * Decode all the metadata entries from a {@link ByteBuf} into a {@link List} of {@link Entry}. - * This is only possible on frame types used to initiate interactions, if the SETUP metadata - * mime type was {@link WellKnownMimeType#MESSAGE_RSOCKET_COMPOSITE_METADATA}. - * - *

Each entry's {@link Entry#getMetadata() content} is a {@link ByteBuf#readSlice(int) slice} - * of the original buffer that can also be {@link ByteBuf#readRetainedSlice(int) retained} if - * needed. - * - *

The buffer is assumed to contain just enough bytes to represent one or more entries (mime - * type compressed or not). The decoding stops when the buffer reaches 0 readable bytes, and - * fails if it contains bytes but not enough to correctly decode an entry. - * - * @param buffer the buffer to decode - * @param retainMetadataSlices should each slide be retained when read from the original buffer? - * @return the {@link List} of decoded {@link Entry} - */ - static List decodeAll(ByteBuf buffer, boolean retainMetadataSlices) { - List list = new ArrayList<>(); - Entry nextEntry = decode(buffer, retainMetadataSlices); - while (nextEntry != null) { - list.add(nextEntry); - nextEntry = decode(buffer, retainMetadataSlices); - } - return list; - } - - private final String mimeString; - private final byte mimeCode; - private final ByteBuf content; - - public Entry(@Nullable String mimeString, byte mimeCode, ByteBuf content) { - this.mimeString = mimeString; - this.mimeCode = mimeCode; - this.content = content; - } - - /** - * Returns the mime type {@link String} representation if there is one. - * - *

A {@code null} value should only occur with a positive {@link #getMimeId()}, denoting an - * entry that is compressed but unparseable (see {@link #rawCompressedMime(byte, ByteBuf)}). - * - * @return the mime type for this entry, or null - */ - @Nullable - public String getMimeType() { - return this.mimeString; - } - - /** @return the compressed mime id byte if relevant (0-127), or -1 if not */ - public byte getMimeId() { - return this.mimeCode; - } - - /** @return the metadata content of this entry */ - public ByteBuf getMetadata() { - return this.content; - } - - /** - * Encode this {@link Entry} into a {@link CompositeByteBuf} representing a composite metadata. - * This buffer may already hold components for previous {@link Entry entries}. - * - * @param compositeByteBuf the {@link CompositeByteBuf} to hold the components of the whole - * composite metadata - * @param byteBufAllocator the {@link ByteBufAllocator} to use to allocate new buffers as needed - */ - public void encodeInto(CompositeByteBuf compositeByteBuf, ByteBufAllocator byteBufAllocator) { - if (this.mimeCode >= 0) { - encodeAndAddMetadata(compositeByteBuf, byteBufAllocator, this.mimeCode, this.content); - } else { - encodeAndAddMetadata(compositeByteBuf, byteBufAllocator, this.mimeString, this.content); - } - } - } } diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java new file mode 100644 index 000000000..2c46aa29a --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java @@ -0,0 +1,145 @@ +package io.rsocket.metadata; + +import static org.assertj.core.api.Assertions.*; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import io.rsocket.test.util.ByteBufUtils; +import io.rsocket.util.NumberUtils; +import java.util.NoSuchElementException; +import org.junit.jupiter.api.Test; + +class CompositeMetadataTest { + + @Test + void decodeEntryTooShortForMimeLength() { + ByteBuf fakeEntry = Unpooled.buffer(); + fakeEntry.writeByte(120); + CompositeMetadata compositeMetadata = new CompositeMetadata(fakeEntry, false); + + assertThatIllegalArgumentException() + .isThrownBy(compositeMetadata::decodeNext) + .withMessage("composite metadata entry buffer is too short to contain proper entry"); + } + + @Test + void decodeEntryHasNoContentLength() { + ByteBuf fakeEntry = Unpooled.buffer(); + fakeEntry.writeByte(0); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); + CompositeMetadata compositeMetadata = new CompositeMetadata(fakeEntry, false); + + assertThatIllegalArgumentException() + .isThrownBy(compositeMetadata::decodeNext) + .withMessage("composite metadata entry buffer is too short to contain proper entry"); + } + + @Test + void decodeEntryTooShortForContentLength() { + ByteBuf fakeEntry = Unpooled.buffer(); + fakeEntry.writeByte(1); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); + NumberUtils.encodeUnsignedMedium(fakeEntry, 456); + fakeEntry.writeChar('w'); + CompositeMetadata compositeMetadata = new CompositeMetadata(fakeEntry, false); + + assertThatIllegalArgumentException() + .isThrownBy(compositeMetadata::decodeNext) + .withMessage("composite metadata entry buffer is too short to contain proper entry"); + } + + @Test + void decodeEntryOnDoneBufferThrowsNoSuchElement() { + ByteBuf fakeBuffer = ByteBufUtils.getRandomByteBuf(0); + CompositeMetadata compositeMetadata = new CompositeMetadata(fakeBuffer, false); + + assertThatExceptionOfType(NoSuchElementException.class) + .isThrownBy(compositeMetadata::decodeNext) + .withMessage("composite metadata has no more entries"); + } + + @Test + void decodeThreeEntries() { + // metadata 1: well known + WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; + ByteBuf metadata1 = Unpooled.buffer(); + metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); + + // metadata 2: custom + String mimeType2 = "application/custom"; + ByteBuf metadata2 = Unpooled.buffer(); + metadata2.writeChar('E'); + metadata2.writeChar('∑'); + metadata2.writeChar('é'); + metadata2.writeBoolean(true); + metadata2.writeChar('W'); + + // metadata 3: reserved but unknown + byte reserved = 120; + assertThat(WellKnownMimeType.fromId(reserved)) + .as("ensure UNKNOWN RESERVED used in test") + .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); + ByteBuf metadata3 = Unpooled.buffer(); + metadata3.writeByte(88); + + CompositeByteBuf compositeMetadataBuffer = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeMetadataFlyweight.encodeAndAddMetadata( + compositeMetadataBuffer, ByteBufAllocator.DEFAULT, mimeType1, metadata1); + CompositeMetadataFlyweight.encodeAndAddMetadata( + compositeMetadataBuffer, ByteBufAllocator.DEFAULT, mimeType2, metadata2); + CompositeMetadataFlyweight.encodeAndAddMetadata( + compositeMetadataBuffer, ByteBufAllocator.DEFAULT, reserved, metadata3); + + CompositeMetadata compositeMetadata = new CompositeMetadata(compositeMetadataBuffer, true); + + compositeMetadata.decodeNext(); + assertThat(compositeMetadata) + .as("entry1") + .isNotNull() + .satisfies( + e -> + assertThat(e.getCurrentMimeType()) + .as("entry1 mime type") + .isEqualTo(mimeType1.getMime())) + .satisfies( + e -> + assertThat(e.getCurrentMimeId()) + .as("entry1 mime id") + .isEqualTo((byte) mimeType1.getIdentifier())) + .satisfies( + e -> + assertThat(e.getCurrentContent().toString(CharsetUtil.UTF_8)) + .as("entry1 decoded") + .isEqualTo("abcdefghijkl")); + + compositeMetadata.decodeNext(); + assertThat(compositeMetadata) + .as("entry2") + .isNotNull() + .satisfies( + e -> assertThat(e.getCurrentMimeType()).as("entry2 mime type").isEqualTo(mimeType2)) + .satisfies(e -> assertThat(e.getCurrentMimeId()).as("entry2 mime id").isEqualTo((byte) -1)) + .satisfies( + e -> + assertThat(e.getCurrentContent()) + .as("entry2 decoded") + .isEqualByComparingTo(metadata2)); + + compositeMetadata.decodeNext(); + assertThat(compositeMetadata) + .as("entry3") + .isNotNull() + .satisfies(e -> assertThat(e.getCurrentMimeType()).as("entry3 mime type").isNull()) + .satisfies(e -> assertThat(e.getCurrentMimeId()).as("entry3 mime id").isEqualTo(reserved)) + .satisfies( + e -> + assertThat(e.getCurrentContent()) + .as("entry3 decoded") + .isEqualByComparingTo(metadata3)); + + assertThat(compositeMetadata.hasNext()).as("has no more than 3 entries").isFalse(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java deleted file mode 100644 index d0e2d405c..000000000 --- a/rsocket-core/src/test/java/io/rsocket/metadata/EntryTest.java +++ /dev/null @@ -1,300 +0,0 @@ -package io.rsocket.metadata; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.CompositeByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.util.CharsetUtil; -import io.rsocket.metadata.CompositeMetadataFlyweight.Entry; -import io.rsocket.test.util.ByteBufUtils; -import io.rsocket.util.NumberUtils; -import java.util.List; -import org.junit.jupiter.api.Test; - -class EntryTest { - - @Test - void encodeEntryWellKnownMetadata() { - WellKnownMimeType type = WellKnownMimeType.fromId(5); - // 5 = 0b00000101 - byte expected = (byte) 0b10000101; - - ByteBuf content = ByteBufUtils.getRandomByteBuf(2); - Entry entry = new Entry(type.getMime(), type.getIdentifier(), content); - - final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - entry.encodeInto(metadata, ByteBufAllocator.DEFAULT); - - assertThat(metadata.readByte()).as("mime header").isEqualTo(expected); - assertThat(metadata.readUnsignedMedium()).as("length header").isEqualTo(2); - assertThat(metadata.readSlice(2)).as("content").isEqualByComparingTo(content); - } - - @Test - void encodeEntryCustomMetadata() { - // length 3, encoded as length - 1 since 0 is not authorized - byte expected = (byte) 2; - ByteBuf content = ByteBufUtils.getRandomByteBuf(2); - Entry entry = new Entry("foo", (byte) -1, content); - - final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - entry.encodeInto(metadata, ByteBufAllocator.DEFAULT); - - assertThat(metadata.readByte()).as("mime header").isEqualTo(expected); - assertThat(metadata.readCharSequence(3, CharsetUtil.US_ASCII).toString()).isEqualTo("foo"); - assertThat(metadata.readUnsignedMedium()).as("length header").isEqualTo(2); - assertThat(metadata.readSlice(2)).as("content").isEqualByComparingTo(content); - } - - @Test - void encodeEntryPassthroughMetadata() { - // 120 = 0b01111000 - byte expected = (byte) 0b11111000; - - ByteBuf content = ByteBufUtils.getRandomByteBuf(2); - Entry entry = new Entry(null, (byte) 120, content); - - final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - entry.encodeInto(metadata, ByteBufAllocator.DEFAULT); - - assertThat(metadata.readByte()).as("mime header").isEqualTo(expected); - assertThat(metadata.readUnsignedMedium()).as("length header").isEqualTo(2); - assertThat(metadata.readSlice(2)).as("content").isEqualByComparingTo(content); - } - - @Test - void decodeEntryTooShortForMimeLength() { - ByteBuf fakeEntry = Unpooled.buffer(); - fakeEntry.writeByte(120); - - assertThatIllegalArgumentException() - .isThrownBy(() -> Entry.decode(fakeEntry, false)) - .withMessage("composite metadata entry buffer is too short to contain proper entry"); - } - - @Test - void decodeEntryHasNoContentLength() { - ByteBuf fakeEntry = Unpooled.buffer(); - fakeEntry.writeByte(0); - fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); - - assertThatIllegalArgumentException() - .isThrownBy(() -> Entry.decode(fakeEntry, false)) - .withMessage("composite metadata entry buffer is too short to contain proper entry"); - } - - @Test - void decodeEntryTooShortForContentLength() { - ByteBuf fakeEntry = Unpooled.buffer(); - fakeEntry.writeByte(1); - fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); - NumberUtils.encodeUnsignedMedium(fakeEntry, 456); - fakeEntry.writeChar('w'); - - assertThatIllegalArgumentException() - .isThrownBy(() -> Entry.decode(fakeEntry, false)) - .withMessage("composite metadata entry buffer is too short to contain proper entry"); - } - - @Test - void decodeEntryOnDoneBufferReturnsNull() { - ByteBuf fakeBuffer = ByteBufUtils.getRandomByteBuf(0); - - assertThat(Entry.decode(fakeBuffer, false)).as("empty entry").isNull(); - } - - @Test - void decodeThreeEntries() { - // metadata 1: well known - WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; - ByteBuf metadata1 = Unpooled.buffer(); - metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); - - // metadata 2: custom - String mimeType2 = "application/custom"; - ByteBuf metadata2 = Unpooled.buffer(); - metadata2.writeChar('E'); - metadata2.writeChar('∑'); - metadata2.writeChar('é'); - metadata2.writeBoolean(true); - metadata2.writeChar('W'); - - // metadata 3: reserved but unknown - byte reserved = 120; - assertThat(WellKnownMimeType.fromId(reserved)) - .as("ensure UNKNOWN RESERVED used in test") - .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); - ByteBuf metadata3 = Unpooled.buffer(); - metadata3.writeByte(88); - - CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - CompositeMetadataFlyweight.encodeAndAddMetadata( - compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); - CompositeMetadataFlyweight.encodeAndAddMetadata( - compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); - CompositeMetadataFlyweight.encodeAndAddMetadata( - compositeMetadata, ByteBufAllocator.DEFAULT, reserved, metadata3); - - Entry entry1 = Entry.decode(compositeMetadata, true); - Entry entry2 = Entry.decode(compositeMetadata, true); - Entry entry3 = Entry.decode(compositeMetadata, true); - Entry expectedNoMoreEntries = Entry.decode(compositeMetadata, true); - - assertThat(expectedNoMoreEntries).as("decodes exactly 3").isNull(); - assertThat(entry1) - .as("entry1") - .isNotNull() - .satisfies( - e -> assertThat(e.getMimeType()).as("entry1 mime type").isEqualTo(mimeType1.getMime())) - .satisfies( - e -> - assertThat(e.getMimeId()) - .as("entry1 mime id") - .isEqualTo((byte) mimeType1.getIdentifier())) - .satisfies( - e -> - assertThat(e.getMetadata().toString(CharsetUtil.UTF_8)) - .as("entry1 decoded") - .isEqualTo("abcdefghijkl")); - - assertThat(entry2) - .as("entry2") - .isNotNull() - .satisfies(e -> assertThat(e.getMimeType()).as("entry2 mime type").isEqualTo(mimeType2)) - .satisfies(e -> assertThat(e.getMimeId()).as("entry2 mime id").isEqualTo((byte) -1)) - .satisfies( - e -> assertThat(e.getMetadata()).as("entry2 decoded").isEqualByComparingTo(metadata2)); - - assertThat(entry3) - .as("entry3") - .isNotNull() - .satisfies(e -> assertThat(e.getMimeType()).as("entry3 mime type").isNull()) - .satisfies(e -> assertThat(e.getMimeId()).as("entry3 mime id").isEqualTo(reserved)) - .satisfies( - e -> assertThat(e.getMetadata()).as("entry3 decoded").isEqualByComparingTo(metadata3)); - } - - @Test - void decodeAllEntries() { - // metadata 1: well known - WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; - ByteBuf metadata1 = Unpooled.buffer(); - metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); - - // metadata 2: custom - String mimeType2 = "application/custom"; - ByteBuf metadata2 = Unpooled.buffer(); - metadata2.writeChar('E'); - metadata2.writeChar('∑'); - metadata2.writeChar('é'); - metadata2.writeBoolean(true); - metadata2.writeChar('W'); - - // metadata 3: reserved but unknown - byte reserved = 120; - assertThat(WellKnownMimeType.fromId(reserved)) - .as("ensure UNKNOWN RESERVED used in test") - .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); - ByteBuf metadata3 = Unpooled.buffer(); - metadata3.writeByte(88); - - CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); - CompositeMetadataFlyweight.encodeAndAddMetadata( - compositeMetadata, ByteBufAllocator.DEFAULT, mimeType1, metadata1); - CompositeMetadataFlyweight.encodeAndAddMetadata( - compositeMetadata, ByteBufAllocator.DEFAULT, mimeType2, metadata2); - CompositeMetadataFlyweight.encodeAndAddMetadata( - compositeMetadata, ByteBufAllocator.DEFAULT, reserved, metadata3); - - List decoded = Entry.decodeAll(compositeMetadata, true); - - assertThat(decoded).as("decodes exactly 3").hasSize(3); - - assertThat(decoded.get(0)) - .as("entry1") - .isNotNull() - .satisfies( - e -> assertThat(e.getMimeType()).as("entry1 mime type").isEqualTo(mimeType1.getMime())) - .satisfies( - e -> - assertThat(e.getMimeId()) - .as("entry1 mime id") - .isEqualTo((byte) mimeType1.getIdentifier())) - .satisfies( - e -> - assertThat(e.getMetadata().toString(CharsetUtil.UTF_8)) - .as("entry1 decoded") - .isEqualTo("abcdefghijkl")); - - assertThat(decoded.get(1)) - .as("entry2") - .isNotNull() - .satisfies(e -> assertThat(e.getMimeType()).as("entry2 mime type").isEqualTo(mimeType2)) - .satisfies(e -> assertThat(e.getMimeId()).as("entry2 mime id").isEqualTo((byte) -1)) - .satisfies( - e -> assertThat(e.getMetadata()).as("entry2 decoded").isEqualByComparingTo(metadata2)); - - assertThat(decoded.get(2)) - .as("entry3") - .isNotNull() - .satisfies(e -> assertThat(e.getMimeType()).as("entry3 mime type").isNull()) - .satisfies(e -> assertThat(e.getMimeId()).as("entry3 mime id").isEqualTo(reserved)) - .satisfies( - e -> assertThat(e.getMetadata()).as("entry3 decoded").isEqualByComparingTo(metadata3)); - } - - @Test - void decodeAllForEmpty() { - ByteBuf emptyBuffer = ByteBufAllocator.DEFAULT.buffer(0); - assertThat(Entry.decodeAll(emptyBuffer, false)).isEmpty(); - } - - @Test - void decodeAllForMalformed() { - CompositeByteBuf compositeByteBuf = ByteBufAllocator.DEFAULT.compositeBuffer(); - // encode a first valid metadata - WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; - ByteBuf metadata1 = Unpooled.buffer(); - metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); - CompositeMetadataFlyweight.encodeAndAddMetadata( - compositeByteBuf, ByteBufAllocator.DEFAULT, mimeType1, metadata1); - // encode an invalid metadata - compositeByteBuf.addComponents(true, ByteBufUtils.getRandomByteBuf(15)); - - assertThatIllegalArgumentException() - .isThrownBy(() -> Entry.decodeAll(compositeByteBuf, false)) - .withMessage("composite metadata entry buffer is too short to contain proper entry"); - } - - @Test - void createCustomTypeEntry() { - Entry entry = Entry.customMime("example/mime", ByteBufUtils.getRandomByteBuf(5)); - - assertThat(entry.getMimeType()).as("mime type").isEqualTo("example/mime"); - assertThat(entry.getMimeId()).as("mime id").isEqualTo((byte) -1); - assertThat(entry.getMetadata().isReadable(5)).as("5 bytes content").isTrue(); - } - - @Test - void createWellKnownTypeEntry() { - WellKnownMimeType wkn = WellKnownMimeType.APPLICATION_XML; - Entry entry = Entry.wellKnownMime(wkn, ByteBufUtils.getRandomByteBuf(5)); - - assertThat(entry.getMimeType()).as("mime type").isEqualTo(wkn.getMime()); - assertThat(entry.getMimeId()).as("mime id").isEqualTo(wkn.getIdentifier()); - assertThat(entry.getMetadata().isReadable(5)).as("5 bytes content").isTrue(); - } - - @Test - void createCompressedRawTypeEntry() { - byte id = (byte) 120; - Entry entry = Entry.rawCompressedMime(id, ByteBufUtils.getRandomByteBuf(5)); - - assertThat(entry.getMimeType()).as("mime type").isNull(); - assertThat(entry.getMimeId()).as("mime id").isEqualTo(id); - assertThat(entry.getMetadata().isReadable(5)).as("5 bytes content").isTrue(); - } -} From 3dd15678194575da98d0719028f73184c7fa3dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Thu, 6 Jun 2019 18:24:15 +0200 Subject: [PATCH 22/25] Add an encode flyweight method that attempts compressing String MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a qol method that will first check if the String passed can be mapped to a WellKnownMimeType (and thus compressed to a 1 byte representation). Signed-off-by: Simon Baslé --- .../metadata/CompositeMetadataFlyweight.java | 39 +++++++++- .../CompositeMetadataFlyweightTest.java | 76 ++++++------------- 2 files changed, 61 insertions(+), 54 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java index dc741a723..97768059e 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java @@ -236,7 +236,44 @@ static ByteBuf encodeMetadataHeader( /** * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf - * buffer}. + * buffer}, first verifying if the passed {@link String} matches a {@link WellKnownMimeType} (in + * which case it will be encoded in a compressed fashion using the mime id of that type). + * + *

Prefer using {@link #encodeAndAddMetadata(CompositeByteBuf, ByteBufAllocator, String, + * ByteBuf)} if you already know that the mime type is not a {@link WellKnownMimeType}. + * + * @param compositeMetaData the buffer that will hold all composite metadata information. + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param mimeType the mime type to encode, as a {@link String}. well known mime types are + * compressed. + * @param metadata the metadata value to encode. + * @see #encodeAndAddMetadata(CompositeByteBuf, ByteBufAllocator, WellKnownMimeType, ByteBuf) + */ + // see #encodeMetadataHeader(ByteBufAllocator, String, int) + public static void encodeAndAddMetadataWithCompression( + CompositeByteBuf compositeMetaData, + ByteBufAllocator allocator, + String mimeType, + ByteBuf metadata) { + WellKnownMimeType wkn = WellKnownMimeType.fromMimeType(mimeType); + if (wkn == WellKnownMimeType.UNPARSEABLE_MIME_TYPE) { + compositeMetaData.addComponents( + true, encodeMetadataHeader(allocator, mimeType, metadata.readableBytes()), metadata); + } else { + compositeMetaData.addComponents( + true, + encodeMetadataHeader(allocator, wkn.getIdentifier(), metadata.readableBytes()), + metadata); + } + } + + /** + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf + * buffer}, without checking if the {@link String} can be matched with a well known compressable + * mime type. Prefer using this method and {@link #encodeAndAddMetadata(CompositeByteBuf, + * ByteBufAllocator, WellKnownMimeType, ByteBuf)} if you know in advance whether or not the mime + * is well known. Otherwise use {@link #encodeAndAddMetadataWithCompression(CompositeByteBuf, + * ByteBufAllocator, String, ByteBuf)} * * @param compositeMetaData the buffer that will hold all composite metadata information. * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java index ade0bd9f8..f450b12f9 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java @@ -442,58 +442,28 @@ void encodeMetadataCustomTypeDelegates() { assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); } - // @Test - // void decodeMetadataLengthFromUntouchedWithKnownMime() { - // ByteBuf encoded = - // CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, - // WellKnownMimeType.APPLICATION_GZIP.getIdentifier(), 12); - // - // assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)) - // .withFailMessage("should not correctly decode if not at correct reader index") - // .isNotEqualTo(12); - // } - // - // @Test - // void decodeMetadataLengthFromMimeDecodedWithKnownMime() { - // ByteBuf encoded = - // CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, - // WellKnownMimeType.APPLICATION_GZIP.getIdentifier(), 12); - // CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded); - // - // - // assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)).isEqualTo(12); - // } - // - // @Test - // void decodeMetadataLengthFromUntouchedWithCustomMime() { - // ByteBuf encoded = - // CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo/bar", 12); - // - // assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)) - // .withFailMessage("should not correctly decode if not at correct reader index") - // .isNotEqualTo(12); - // } - // - // @Test - // void decodeMetadataLengthFromMimeDecodedWithCustomMime() { - // ByteBuf encoded = - // CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo/bar", 12); - // CompositeMetadataFlyweight.decode3WaysMimeFromMetadataHeader(encoded); - // - // - // assertThat(CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(encoded)).isEqualTo(12); - // } - // - // @Test - // void decodeMetadataLengthFromTooShortBuffer() { - // ByteBuf buffer = Unpooled.buffer(); - // buffer.writeShort(12); - // - // assertThatExceptionOfType(RuntimeException.class) - // .isThrownBy(() -> - // CompositeMetadataFlyweight.decodeMetadataLengthFromMetadataHeader(buffer)) - // .withMessage("the given buffer should contain at least 3 readable bytes after - // decoding mime type"); - // } + @Test + void encodeTryCompressWithCustomType() { + ByteBuf metadata = ByteBufUtils.getRandomByteBuf(2); + CompositeByteBuf target = UnpooledByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataFlyweight.encodeAndAddMetadataWithCompression( + target, UnpooledByteBufAllocator.DEFAULT, "custom/example", metadata); + + assertThat(target.readableBytes()).as("readableBytes 1 + 14 + 3 + 2").isEqualTo(20); + } + @Test + void encodeTryCompressWithCompressableType() { + ByteBuf metadata = ByteBufUtils.getRandomByteBuf(2); + CompositeByteBuf target = UnpooledByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataFlyweight.encodeAndAddMetadataWithCompression( + target, + UnpooledByteBufAllocator.DEFAULT, + WellKnownMimeType.APPLICATION_AVRO.getMime(), + metadata); + + assertThat(target.readableBytes()).as("readableBytes 1 + 3 + 2").isEqualTo(6); + } } From 1720671fe31abcfa0af8cd93777c390f561db187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Fri, 7 Jun 2019 12:48:35 +0200 Subject: [PATCH 23/25] Avoid moving full buffer's readerIndex when slicing entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This implies that the index from which to decode and slice the next entry MUST be provided by the user. To that end, we provide a method to compute the next entry's index, given the pair of ByteBuf slices returned by decodeMimeAndContentBuffers. Also renamed that method to decodeMimeAndContentBufferSlices, to make it more obvious that it produces slices (readerIndex impact and slices nature are mentioned in the javadoc). CompositeMetadata now keeps the nextEntryIndex as state and uses that for hasNext / decodeNext. Signed-off-by: Simon Baslé --- .../rsocket/metadata/CompositeMetadata.java | 13 ++++++- .../metadata/CompositeMetadataFlyweight.java | 37 ++++++++++++++++--- .../CompositeMetadataFlyweightTest.java | 23 ++++++------ 3 files changed, 54 insertions(+), 19 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java index 7bc733b47..0e8ee2de9 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java @@ -34,6 +34,7 @@ public final class CompositeMetadata { private byte id; private @Nullable String mime; private ByteBuf content; + private int nextEntryIndex; /** * Wrap a composite metadata {@link ByteBuf} to allow incremental decoding of its entries. Each @@ -46,6 +47,10 @@ public final class CompositeMetadata { public CompositeMetadata(ByteBuf fullCompositeMetadataBuffer, boolean retainSlices) { this.source = fullCompositeMetadataBuffer; this.retainSlices = retainSlices; + this.id = -1; + this.mime = null; + this.content = null; + this.nextEntryIndex = 0; } /** @@ -65,7 +70,7 @@ public CompositeMetadata(ByteBuf fullCompositeMetadataBuffer) { * @return true if the source buffer still has readable bytes */ public boolean hasNext() { - return source.isReadable(); + return source.writerIndex() - nextEntryIndex > 0; } private void reset() { @@ -86,7 +91,8 @@ private void reset() { public void decodeNext() { reset(); ByteBuf[] decoded = - CompositeMetadataFlyweight.decodeMimeAndContentBuffers(source, retainSlices); + CompositeMetadataFlyweight.decodeMimeAndContentBuffersSlices( + source, nextEntryIndex, retainSlices); if (decoded == CompositeMetadataFlyweight.METADATA_MALFORMED) { throw new IllegalArgumentException( "composite metadata entry buffer is too short to contain proper entry"); @@ -97,6 +103,9 @@ public void decodeNext() { ByteBuf header = decoded[0]; this.content = decoded[1]; + // move the nextEntryIndex + this.nextEntryIndex = + CompositeMetadataFlyweight.computeNextEntryIndex(this.nextEntryIndex, header, content); if (header.readableBytes() == 1) { this.id = CompositeMetadataFlyweight.decodeMimeIdFromMimeBuffer(header); diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java index 97768059e..bfbbfe143 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java @@ -33,8 +33,14 @@ private CompositeMetadataFlyweight() {} /** * Decode the next metadata entry (a mime header + content pair of {@link ByteBuf}) from a {@link - * ByteBuf} that contains at least enough bytes for one more such entry. The header buffer is - * either: + * ByteBuf} that contains at least enough bytes for one more such entry. These buffers are + * actually slices of the full metadata buffer, and this method doesn't move the full metadata + * buffer's {@link ByteBuf#readerIndex()}. As such, it requires the user to provide an {@code + * index} to read from. The next index is computed by calling {@link #computeNextEntryIndex(int, + * ByteBuf, ByteBuf)}. Size of the first buffer (the "header buffer") drives which decoding method + * should be further applied to it. + * + *

The header buffer is either: * *

    *
  • made up of a single byte: this represents an encoded mime id, which can be further @@ -52,13 +58,19 @@ private CompositeMetadataFlyweight() {} * * @param compositeMetadata the source {@link ByteBuf} that originally contains one or more * metadata entries + * @param entryIndex the {@link ByteBuf#readerIndex()} to start decoding from. original reader + * index is kept on the source buffer * @param retainSlices should produced metadata entry buffers {@link ByteBuf#slice() slices} be * {@link ByteBuf#retainedSlice() retained}? - * @return a {@link ByteBuf} slice array of length 2 containing the mime header buffer and the - * content buffer, or one of the zero-length error constant arrays + * @return a {@link ByteBuf} array of length 2 containing the mime header buffer + * slice and the content buffer slice, or one of the + * zero-length error constant arrays */ - public static ByteBuf[] decodeMimeAndContentBuffers( - ByteBuf compositeMetadata, boolean retainSlices) { + public static ByteBuf[] decodeMimeAndContentBuffersSlices( + ByteBuf compositeMetadata, int entryIndex, boolean retainSlices) { + compositeMetadata.markReaderIndex(); + compositeMetadata.readerIndex(entryIndex); + if (compositeMetadata.isReadable()) { ByteBuf mime; int ridx = compositeMetadata.readerIndex(); @@ -90,6 +102,7 @@ public static ByteBuf[] decodeMimeAndContentBuffers( // which was already skipped in initial read compositeMetadata.skipBytes(mimeLength); } else { + compositeMetadata.resetReaderIndex(); return METADATA_MALFORMED; } } @@ -102,17 +115,29 @@ public static ByteBuf[] decodeMimeAndContentBuffers( retainSlices ? compositeMetadata.readRetainedSlice(metadataLength) : compositeMetadata.readSlice(metadataLength); + compositeMetadata.resetReaderIndex(); return new ByteBuf[] {mime, metadata}; } else { + compositeMetadata.resetReaderIndex(); return METADATA_MALFORMED; } } else { + compositeMetadata.resetReaderIndex(); return METADATA_MALFORMED; } } + compositeMetadata.resetReaderIndex(); return METADATA_BUFFERS_DONE; } + public static int computeNextEntryIndex( + int currentEntryIndex, ByteBuf headerSlice, ByteBuf contentSlice) { + return currentEntryIndex + + headerSlice.readableBytes() // this includes the mime length byte + + 3 // 3 bytes of the content length, which are excluded from the slice + + contentSlice.readableBytes(); + } + /** * Decode a {@code byte} compressed mime id from a {@link ByteBuf}, assuming said buffer properly * contains such an id. diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java index f450b12f9..1139b2a63 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java @@ -41,7 +41,7 @@ void knownMimeHeaderZero_avro() { .isEqualTo("10000000") .isEqualTo(byteToBitsString(mime.getIdentifier()).replaceFirst("0", "1")); - final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); assertThat(byteBufs).hasSize(2).doesNotContainNull(); ByteBuf header = byteBufs[0]; @@ -78,7 +78,7 @@ void knownMimeHeader127_compositeMetadata() { .isEqualTo("11111111") .isEqualTo(byteToBitsString(mime.getIdentifier()).replaceFirst("0", "1")); - final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); assertThat(byteBufs).hasSize(2).doesNotContainNull(); ByteBuf header = byteBufs[0]; @@ -111,7 +111,7 @@ void knownMimeHeader120_reserved() { assertThat(toHeaderBits(encoded)).startsWith("1").isEqualTo("11111000"); - final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); assertThat(byteBufs).hasSize(2).doesNotContainNull(); ByteBuf header = byteBufs[0]; @@ -139,7 +139,7 @@ void customMimeHeaderLengthOne() { // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000000"); - final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); assertThat(byteBufs).hasSize(2).doesNotContainNull(); ByteBuf header = byteBufs[0]; @@ -171,7 +171,7 @@ void customMimeHeaderLengthTwo() { // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000001"); - final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); assertThat(byteBufs).hasSize(2).doesNotContainNull(); ByteBuf header = byteBufs[0]; @@ -209,7 +209,7 @@ void customMimeHeaderLength127() { // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111110"); - final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); assertThat(byteBufs).hasSize(2).doesNotContainNull(); ByteBuf header = byteBufs[0]; @@ -247,7 +247,7 @@ void customMimeHeaderLength128() { // remember actual length = encoded length + 1 assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111111"); - final ByteBuf[] byteBufs = decodeMimeAndContentBuffers(encoded, false); + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); assertThat(byteBufs).hasSize(2).doesNotContainNull(); ByteBuf header = byteBufs[0]; @@ -326,7 +326,7 @@ void decodeEntryTooShortForMimeLength() { ByteBuf fakeEntry = Unpooled.buffer(); fakeEntry.writeByte(120); - assertThat(decodeMimeAndContentBuffers(fakeEntry, false)).isSameAs(METADATA_MALFORMED); + assertThat(decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)).isSameAs(METADATA_MALFORMED); } @Test @@ -335,7 +335,7 @@ void decodeEntryHasNoContentLength() { fakeEntry.writeByte(0); fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); - assertThat(decodeMimeAndContentBuffers(fakeEntry, false)).isSameAs(METADATA_MALFORMED); + assertThat(decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)).isSameAs(METADATA_MALFORMED); } @Test @@ -346,14 +346,15 @@ void decodeEntryTooShortForContentLength() { NumberUtils.encodeUnsignedMedium(fakeEntry, 456); fakeEntry.writeChar('w'); - assertThat(decodeMimeAndContentBuffers(fakeEntry, false)).isSameAs(METADATA_MALFORMED); + assertThat(decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)).isSameAs(METADATA_MALFORMED); } @Test void decodeEntryAtEndOfBuffer() { ByteBuf fakeEntry = Unpooled.buffer(); - assertThat(decodeMimeAndContentBuffers(fakeEntry, false)).isSameAs(METADATA_BUFFERS_DONE); + assertThat(decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)) + .isSameAs(METADATA_BUFFERS_DONE); } @Test From 5d76e102abb853dc7b5f57081181492c8f1a8bb1 Mon Sep 17 00:00:00 2001 From: Ben Hale Date: Tue, 11 Jun 2019 11:02:55 -0400 Subject: [PATCH 24/25] CompositeMetadata as Iterable Previously, the implementation of CompositeMetadata resulted in a mutable type that didn't fit in with either imperative (for) or reactive (fromIterable) iteration strategies. This change updates the type to implement Iterable, and return an Iterator that lazily traverses the ByteBuf. There are also some improvements to the Flyweight in order to support this iteration style. Signed-off-by: Ben Hale --- .../metadata/WellKnownMimeTypePerf.java | 36 +- .../rsocket/metadata/CompositeMetadata.java | 289 ++++++----- .../metadata/CompositeMetadataFlyweight.java | 267 +++++----- .../rsocket/metadata/WellKnownMimeType.java | 170 ++++--- .../CompositeMetadataFlyweightTest.java | 465 ++++++++++-------- .../metadata/CompositeMetadataTest.java | 112 +++-- .../metadata/WellKnownMimeTypeTest.java | 55 ++- 7 files changed, 795 insertions(+), 599 deletions(-) diff --git a/rsocket-core/src/jmh/java/io/rsocket/metadata/WellKnownMimeTypePerf.java b/rsocket-core/src/jmh/java/io/rsocket/metadata/WellKnownMimeTypePerf.java index 75598743d..8f429fc19 100644 --- a/rsocket-core/src/jmh/java/io/rsocket/metadata/WellKnownMimeTypePerf.java +++ b/rsocket-core/src/jmh/java/io/rsocket/metadata/WellKnownMimeTypePerf.java @@ -10,7 +10,7 @@ @State(Scope.Thread) public class WellKnownMimeTypePerf { - // this is the old values() looping implementation of fromId + // this is the old values() looping implementation of fromIdentifier private WellKnownMimeType fromIdValuesLoop(int id) { if (id < 0 || id > 127) { return WellKnownMimeType.UNPARSEABLE_MIME_TYPE; @@ -23,10 +23,10 @@ private WellKnownMimeType fromIdValuesLoop(int id) { return WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE; } - // this is the core of the old values() looping implementation of fromMimeType + // this is the core of the old values() looping implementation of fromString private WellKnownMimeType fromStringValuesLoop(String mimeType) { for (WellKnownMimeType value : WellKnownMimeType.values()) { - if (mimeType.equals(value.getMime())) { + if (mimeType.equals(value.getString())) { return value; } } @@ -36,18 +36,18 @@ private WellKnownMimeType fromStringValuesLoop(String mimeType) { @Benchmark public void fromIdArrayLookup(final Blackhole bh) { // negative lookup - bh.consume(WellKnownMimeType.fromId(-10)); - bh.consume(WellKnownMimeType.fromId(-1)); + bh.consume(WellKnownMimeType.fromIdentifier(-10)); + bh.consume(WellKnownMimeType.fromIdentifier(-1)); // too large lookup - bh.consume(WellKnownMimeType.fromId(129)); + bh.consume(WellKnownMimeType.fromIdentifier(129)); // first lookup - bh.consume(WellKnownMimeType.fromId(0)); + bh.consume(WellKnownMimeType.fromIdentifier(0)); // middle lookup - bh.consume(WellKnownMimeType.fromId(37)); + bh.consume(WellKnownMimeType.fromIdentifier(37)); // reserved lookup - bh.consume(WellKnownMimeType.fromId(63)); + bh.consume(WellKnownMimeType.fromIdentifier(63)); // last lookup - bh.consume(WellKnownMimeType.fromId(127)); + bh.consume(WellKnownMimeType.fromIdentifier(127)); } @Benchmark @@ -70,15 +70,15 @@ public void fromIdValuesLoopLookup(final Blackhole bh) { @Benchmark public void fromStringMapLookup(final Blackhole bh) { // unknown lookup - bh.consume(WellKnownMimeType.fromMimeType("foo/bar")); + bh.consume(WellKnownMimeType.fromString("foo/bar")); // first lookup - bh.consume(WellKnownMimeType.fromMimeType(WellKnownMimeType.APPLICATION_AVRO.getMime())); + bh.consume(WellKnownMimeType.fromString(WellKnownMimeType.APPLICATION_AVRO.getString())); // middle lookup - bh.consume(WellKnownMimeType.fromMimeType(WellKnownMimeType.VIDEO_VP8.getMime())); + bh.consume(WellKnownMimeType.fromString(WellKnownMimeType.VIDEO_VP8.getString())); // last lookup bh.consume( - WellKnownMimeType.fromMimeType( - WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getMime())); + WellKnownMimeType.fromString( + WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString())); } @Benchmark @@ -86,11 +86,11 @@ public void fromStringValuesLoopLookup(final Blackhole bh) { // unknown lookup bh.consume(fromStringValuesLoop("foo/bar")); // first lookup - bh.consume(fromStringValuesLoop(WellKnownMimeType.APPLICATION_AVRO.getMime())); + bh.consume(fromStringValuesLoop(WellKnownMimeType.APPLICATION_AVRO.getString())); // middle lookup - bh.consume(fromStringValuesLoop(WellKnownMimeType.VIDEO_VP8.getMime())); + bh.consume(fromStringValuesLoop(WellKnownMimeType.VIDEO_VP8.getString())); // last lookup bh.consume( - fromStringValuesLoop(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getMime())); + fromStringValuesLoop(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString())); } } diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java index 0e8ee2de9..9dde3eba6 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java @@ -1,154 +1,221 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed 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 io.rsocket.metadata; +import static io.rsocket.metadata.CompositeMetadataFlyweight.computeNextEntryIndex; +import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeAndContentBuffersSlices; +import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeIdFromMimeBuffer; +import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer; +import static io.rsocket.metadata.CompositeMetadataFlyweight.hasEntry; +import static io.rsocket.metadata.CompositeMetadataFlyweight.isWellKnownMimeType; + import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; -import java.util.NoSuchElementException; +import io.rsocket.metadata.CompositeMetadata.Entry; +import java.util.Iterator; import reactor.util.annotation.Nullable; /** - * An iterator-like wrapper around a {@link ByteBuf} that exposes metadata entry information at each - * decoding step. This is only possible on frame types used to initiate interactions, if the SETUP - * metadata mime type was {@link WellKnownMimeType#MESSAGE_RSOCKET_COMPOSITE_METADATA}. + * An {@link Iterable} wrapper around a {@link ByteBuf} that exposes metadata entry information at + * each decoding step. This is only possible on frame types used to initiate interactions, if the + * SETUP metadata mime type was {@link WellKnownMimeType#MESSAGE_RSOCKET_COMPOSITE_METADATA}. * - *

    This allows efficient incremental decoding of the entries (which moves the source's {@link + *

    This allows efficient incremental decoding of the entries (without moving the source's {@link * io.netty.buffer.ByteBuf#readerIndex()}). The buffer is assumed to contain just enough bytes to * represent one or more entries (mime type compressed or not). The decoding stops when the buffer - * reaches 0 readable bytes ({@code hasNext() == false}), and fails if it contains bytes but not - * enough to correctly decode an entry. + * reaches 0 readable bytes, and fails if it contains bytes but not enough to correctly decode an + * entry. * *

    A note on future-proofness: it is possible to come across a compressed mime type that this * implementation doesn't recognize. This is likely to be due to the use of a byte id that is merely * reserved in this implementation, but maps to a {@link WellKnownMimeType} in the implementation - * that encoded the metadata. This can be detected by {@link #getCurrentMimeId()} returning a - * positive {@code byte} while {@link #getCurrentMimeType()} returns {@literal null}. The byte and - * content buffer should be kept around and re-encoded using {@link + * that encoded the metadata. This can be detected by detecting that an entry is a {@link + * ReservedMimeTypeEntry}. In this case {@link Entry#getMimeType()} will return {@code null}. The + * encoded id can be retrieved using {@link ReservedMimeTypeEntry#getType()}. The byte and content + * buffer should be kept around and re-encoded using {@link * CompositeMetadataFlyweight#encodeAndAddMetadata(CompositeByteBuf, ByteBufAllocator, byte, * ByteBuf)} in case passing that entry through is required. */ -public final class CompositeMetadata { +public final class CompositeMetadata implements Iterable { - private final ByteBuf source; private final boolean retainSlices; - private byte id; - private @Nullable String mime; - private ByteBuf content; - private int nextEntryIndex; + private final ByteBuf source; - /** - * Wrap a composite metadata {@link ByteBuf} to allow incremental decoding of its entries. Each - * decoded {@link ByteBuf} is either a {@link ByteBuf#slice()} or a {@link - * ByteBuf#retainedSlice()} of the original buffer, depending on the {@code retainSlices} - * parameter. - * - * @param fullCompositeMetadataBuffer - */ - public CompositeMetadata(ByteBuf fullCompositeMetadataBuffer, boolean retainSlices) { - this.source = fullCompositeMetadataBuffer; + public CompositeMetadata(ByteBuf source, boolean retainSlices) { + this.source = source; this.retainSlices = retainSlices; - this.id = -1; - this.mime = null; - this.content = null; - this.nextEntryIndex = 0; } - /** - * Wrap a composite metadata {@link ByteBuf} to allow incremental decoding of its entries. Each - * decoded {@link ByteBuf} is a {@link ByteBuf#retainedSlice()} of the original buffer. - * - * @param fullCompositeMetadataBuffer - */ - public CompositeMetadata(ByteBuf fullCompositeMetadataBuffer) { - this(fullCompositeMetadataBuffer, true); - } + @Override + public Iterator iterator() { + return new Iterator() { - /** - * Return true if the source buffer still has readable bytes, which is assumed to mean at least - * one more decodable entry. - * - * @return true if the source buffer still has readable bytes - */ - public boolean hasNext() { - return source.writerIndex() - nextEntryIndex > 0; + private int entryIndex = 0; + + @Override + public boolean hasNext() { + return hasEntry(CompositeMetadata.this.source, this.entryIndex); + } + + @Override + public Entry next() { + ByteBuf[] headerAndData = + decodeMimeAndContentBuffersSlices( + CompositeMetadata.this.source, + this.entryIndex, + CompositeMetadata.this.retainSlices); + + ByteBuf header = headerAndData[0]; + ByteBuf data = headerAndData[1]; + + this.entryIndex = computeNextEntryIndex(this.entryIndex, header, data); + + if (!isWellKnownMimeType(header)) { + CharSequence typeString = decodeMimeTypeFromMimeBuffer(header); + if (typeString == null) { + throw new IllegalStateException("MIME type cannot be null"); + } + + return new ExplicitMimeTimeEntry(data, typeString.toString()); + } + + byte id = decodeMimeIdFromMimeBuffer(header); + WellKnownMimeType type = WellKnownMimeType.fromIdentifier(id); + + if (WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE == type) { + return new ReservedMimeTypeEntry(data, id); + } + + return new WellKnownMimeTypeEntry(data, type); + } + }; } - private void reset() { - this.id = -1; - this.mime = null; - this.content = null; + /** An entry in the {@link CompositeMetadata}. */ + public interface Entry { + + /** + * Returns the un-decoded content of the {@link Entry}. + * + * @return the un-decoded content of the {@link Entry} + */ + ByteBuf getContent(); + + /** + * Returns the MIME type of the entry, if it can be decoded. + * + * @return the MIME type of the entry, if it can be decoded, otherwise {@code null}. + */ + @Nullable + String getMimeType(); } - /** - * Decode the next entry in the source buffer, making its values accessible through the {@link - * #getCurrentMimeType()}, {@link #getCurrentMimeId()} and {@link #getCurrentContent()} accessors. - * - * @throws IllegalArgumentException if the buffer contains more data but that data cannot be - * decoded as an entry - * @throws NoSuchElementException if the buffer contains no more data (which can be avoided by - * checking {@link #hasNext()}) - */ - public void decodeNext() { - reset(); - ByteBuf[] decoded = - CompositeMetadataFlyweight.decodeMimeAndContentBuffersSlices( - source, nextEntryIndex, retainSlices); - if (decoded == CompositeMetadataFlyweight.METADATA_MALFORMED) { - throw new IllegalArgumentException( - "composite metadata entry buffer is too short to contain proper entry"); + /** An {@link Entry} backed by an explicitly declared MIME type. */ + public static final class ExplicitMimeTimeEntry implements Entry { + + private final ByteBuf content; + + private final String type; + + public ExplicitMimeTimeEntry(ByteBuf content, String type) { + this.content = content; + this.type = type; } - if (decoded == CompositeMetadataFlyweight.METADATA_BUFFERS_DONE) { - throw new NoSuchElementException("composite metadata has no more entries"); + + @Override + public ByteBuf getContent() { + return this.content; } - ByteBuf header = decoded[0]; - this.content = decoded[1]; - // move the nextEntryIndex - this.nextEntryIndex = - CompositeMetadataFlyweight.computeNextEntryIndex(this.nextEntryIndex, header, content); - - if (header.readableBytes() == 1) { - this.id = CompositeMetadataFlyweight.decodeMimeIdFromMimeBuffer(header); - WellKnownMimeType wkn = WellKnownMimeType.fromId(id); - if (wkn != WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE) { - this.mime = wkn.getMime(); - } - } else { - this.id = -1; - CharSequence charSequence = CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header); - if (charSequence == null) { - throw new IllegalArgumentException( - "composite metadata entry parsing failed on custom type"); - } - this.mime = charSequence.toString(); + @Override + public String getMimeType() { + return this.type; } } /** - * Returns the mime type {@link String} representation of the currently decoded entry, if there is - * one. - * - *

    A {@code null} value should only occur with a positive {@link #getCurrentMimeId()}, denoting - * an entry that is compressed but unparseable (probably a buffer encoded by another version of - * the well known mime type extension spec, which is only reserved in this implementation). - * - * @return the mime type for this entry, or null + * An {@link Entry} backed by a {@link WellKnownMimeType} entry, but one that is not understood by + * this implementation. */ - @Nullable - public String getCurrentMimeType() { - return this.mime; - } + public static final class ReservedMimeTypeEntry implements Entry { + private final ByteBuf content; + private final int type; - /** - * @return the compressed mime id byte if relevant (0-127), or -1 if not (custom mime type as - * {@link String}) - */ - public byte getCurrentMimeId() { - return this.id; + public ReservedMimeTypeEntry(ByteBuf content, int type) { + this.content = content; + this.type = type; + } + + @Override + public ByteBuf getContent() { + return this.content; + } + + /** + * {@inheritDoc} + * Since this entry represents a compressed id that couldn't be decoded, this + * is always {@code null}. + */ + @Override + public String getMimeType() { + return null; + } + + /** + * Returns the reserved, but unknown {@link WellKnownMimeType} for this entry. + * Range is 0-127 (inclusive). + * + * @return the reserved, but unknown {@link WellKnownMimeType} for this entry + */ + public int getType() { + return this.type; + } } - /** @return the metadata content of the currently decoded entry */ - public ByteBuf getCurrentContent() { - return this.content; + /** An {@link Entry} backed by a {@link WellKnownMimeType}. */ + public static final class WellKnownMimeTypeEntry implements Entry { + + private final ByteBuf content; + private final WellKnownMimeType type; + + public WellKnownMimeTypeEntry(ByteBuf content, WellKnownMimeType type) { + this.content = content; + this.type = type; + } + + @Override + public ByteBuf getContent() { + return this.content; + } + + @Override + public String getMimeType() { + return this.type.getString(); + } + + /** + * Returns the {@link WellKnownMimeType} for this entry. + * + * @return the {@link WellKnownMimeType} for this entry + */ + public WellKnownMimeType getType() { + return this.type; + } } } diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java index bfbbfe143..9abd638cb 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed 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 io.rsocket.metadata; import io.netty.buffer.ByteBuf; @@ -16,21 +32,19 @@ public class CompositeMetadataFlyweight { static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 - static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 - /** - * Denotes that an attempt at 0-garbage decoding failed because the input buffer didn't have - * enough bytes to represent a complete metadata entry, only part of the bytes. - */ - public static final ByteBuf[] METADATA_MALFORMED = new ByteBuf[0]; - /** - * Denotes that an attempt at garbage-free decoding failed because the input buffer was completely - * empty, which generally means that no more entries are present in the buffer. - */ - public static final ByteBuf[] METADATA_BUFFERS_DONE = new ByteBuf[0]; + static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 private CompositeMetadataFlyweight() {} + public static int computeNextEntryIndex( + int currentEntryIndex, ByteBuf headerSlice, ByteBuf contentSlice) { + return currentEntryIndex + + headerSlice.readableBytes() // this includes the mime length byte + + 3 // 3 bytes of the content length, which are excluded from the slice + + contentSlice.readableBytes(); + } + /** * Decode the next metadata entry (a mime header + content pair of {@link ByteBuf}) from a {@link * ByteBuf} that contains at least enough bytes for one more such entry. These buffers are @@ -51,11 +65,6 @@ private CompositeMetadataFlyweight() {} * remaining length of the buffer is that of the mime string. *

* - * Moreover, if the source buffer is empty of readable bytes it is assumed that the composite has - * been decoded entirely and the {@link #METADATA_BUFFERS_DONE} constant is returned. If the - * buffer contains some readable bytes but not enough for a correct representation of an - * entry, the {@link #METADATA_MALFORMED} constant is returned. - * * @param compositeMetadata the source {@link ByteBuf} that originally contains one or more * metadata entries * @param entryIndex the {@link ByteBuf#readerIndex()} to start decoding from. original reader @@ -103,7 +112,7 @@ public static ByteBuf[] decodeMimeAndContentBuffersSlices( compositeMetadata.skipBytes(mimeLength); } else { compositeMetadata.resetReaderIndex(); - return METADATA_MALFORMED; + throw new IllegalStateException("metadata is malformed"); } } @@ -119,23 +128,16 @@ public static ByteBuf[] decodeMimeAndContentBuffersSlices( return new ByteBuf[] {mime, metadata}; } else { compositeMetadata.resetReaderIndex(); - return METADATA_MALFORMED; + throw new IllegalStateException("metadata is malformed"); } } else { compositeMetadata.resetReaderIndex(); - return METADATA_MALFORMED; + throw new IllegalStateException("metadata is malformed"); } } compositeMetadata.resetReaderIndex(); - return METADATA_BUFFERS_DONE; - } - - public static int computeNextEntryIndex( - int currentEntryIndex, ByteBuf headerSlice, ByteBuf contentSlice) { - return currentEntryIndex - + headerSlice.readableBytes() // this includes the mime length byte - + 3 // 3 bytes of the content length, which are excluded from the slice - + contentSlice.readableBytes(); + throw new IllegalArgumentException( + String.format("entry index %d is larger than buffer size", entryIndex)); } /** @@ -181,7 +183,7 @@ public static byte decodeMimeIdFromMimeBuffer(ByteBuf mimeBuffer) { @Nullable public static CharSequence decodeMimeTypeFromMimeBuffer(ByteBuf flyweightMimeBuffer) { if (flyweightMimeBuffer.readableBytes() < 2) { - return null; + throw new IllegalStateException("unable to decode explicit MIME type"); } // the encoded length is assumed to be kept at the start of the buffer // but also assumed to be irrelevant because the rest of the slice length @@ -192,71 +194,47 @@ public static CharSequence decodeMimeTypeFromMimeBuffer(ByteBuf flyweightMimeBuf } /** - * Encode a {@link WellKnownMimeType well known mime type} and a metadata value length into a - * newly allocated {@link ByteBuf}. - * - *

This compact representation encodes the mime type via its ID on a single byte, and the - * unsigned value length on 3 additional bytes. + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf + * buffer}, without checking if the {@link String} can be matched with a well known compressable + * mime type. Prefer using this method and {@link #encodeAndAddMetadata(CompositeByteBuf, + * ByteBufAllocator, WellKnownMimeType, ByteBuf)} if you know in advance whether or not the mime + * is well known. Otherwise use {@link #encodeAndAddMetadataWithCompression(CompositeByteBuf, + * ByteBufAllocator, String, ByteBuf)} * - * @param allocator the {@link ByteBufAllocator} to use to create the buffer. - * @param mimeType a byte identifier of a {@link WellKnownMimeType} to encode. - * @param metadataLength the metadata length to append to the buffer as an unsigned 24 bits - * integer. - * @return the encoded mime and metadata length information + * @param compositeMetaData the buffer that will hold all composite metadata information. + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param customMimeType the custom mime type to encode. + * @param metadata the metadata value to encode. */ - static ByteBuf encodeMetadataHeader( - ByteBufAllocator allocator, byte mimeType, int metadataLength) { - ByteBuf buffer = allocator.buffer(4, 4).writeByte(mimeType | STREAM_METADATA_KNOWN_MASK); - - NumberUtils.encodeUnsignedMedium(buffer, metadataLength); - - return buffer; + // see #encodeMetadataHeader(ByteBufAllocator, String, int) + public static void encodeAndAddMetadata( + CompositeByteBuf compositeMetaData, + ByteBufAllocator allocator, + String customMimeType, + ByteBuf metadata) { + compositeMetaData.addComponents( + true, encodeMetadataHeader(allocator, customMimeType, metadata.readableBytes()), metadata); } /** - * Encode a custom mime type and a metadata value length into a newly allocated {@link ByteBuf}. - * - *

This larger representation encodes the mime type representation's length on a single byte, - * then the representation itself, then the unsigned metadata value length on 3 additional bytes. + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf + * buffer}. * - * @param allocator the {@link ByteBufAllocator} to use to create the buffer. - * @param customMime a custom mime type to encode. - * @param metadataLength the metadata length to append to the buffer as an unsigned 24 bits - * integer. - * @return the encoded mime and metadata length information + * @param compositeMetaData the buffer that will hold all composite metadata information. + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param knownMimeType the {@link WellKnownMimeType} to encode. + * @param metadata the metadata value to encode. */ - static ByteBuf encodeMetadataHeader( - ByteBufAllocator allocator, String customMime, int metadataLength) { - ByteBuf metadataHeader = allocator.buffer(4 + customMime.length()); - // reserve 1 byte for the customMime length - int writerIndexInitial = metadataHeader.writerIndex(); - metadataHeader.writerIndex(writerIndexInitial + 1); - - // write the custom mime in UTF8 but validate it is all ASCII-compatible - // (which produces the right result since ASCII chars are still encoded on 1 byte in UTF8) - int customMimeLength = ByteBufUtil.writeUtf8(metadataHeader, customMime); - if (!ByteBufUtil.isText(metadataHeader, CharsetUtil.US_ASCII)) { - metadataHeader.release(); - throw new IllegalArgumentException("custom mime type must be US_ASCII characters only"); - } - if (customMimeLength < 1 || customMimeLength > 128) { - metadataHeader.release(); - throw new IllegalArgumentException( - "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); - } - metadataHeader.markWriterIndex(); - - // go back to beginning and write the length - // encoded length is one less than actual length, since 0 is never a valid length, which gives - // wider representation range - metadataHeader.writerIndex(writerIndexInitial); - metadataHeader.writeByte(customMimeLength - 1); - - // go back to post-mime type and write the metadata content length - metadataHeader.resetWriterIndex(); - NumberUtils.encodeUnsignedMedium(metadataHeader, metadataLength); - - return metadataHeader; + // see #encodeMetadataHeader(ByteBufAllocator, byte, int) + public static void encodeAndAddMetadata( + CompositeByteBuf compositeMetaData, + ByteBufAllocator allocator, + WellKnownMimeType knownMimeType, + ByteBuf metadata) { + compositeMetaData.addComponents( + true, + encodeMetadataHeader(allocator, knownMimeType.getIdentifier(), metadata.readableBytes()), + metadata); } /** @@ -280,7 +258,7 @@ public static void encodeAndAddMetadataWithCompression( ByteBufAllocator allocator, String mimeType, ByteBuf metadata) { - WellKnownMimeType wkn = WellKnownMimeType.fromMimeType(mimeType); + WellKnownMimeType wkn = WellKnownMimeType.fromString(mimeType); if (wkn == WellKnownMimeType.UNPARSEABLE_MIME_TYPE) { compositeMetaData.addComponents( true, encodeMetadataHeader(allocator, mimeType, metadata.readableBytes()), metadata); @@ -293,47 +271,24 @@ public static void encodeAndAddMetadataWithCompression( } /** - * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf - * buffer}, without checking if the {@link String} can be matched with a well known compressable - * mime type. Prefer using this method and {@link #encodeAndAddMetadata(CompositeByteBuf, - * ByteBufAllocator, WellKnownMimeType, ByteBuf)} if you know in advance whether or not the mime - * is well known. Otherwise use {@link #encodeAndAddMetadataWithCompression(CompositeByteBuf, - * ByteBufAllocator, String, ByteBuf)} + * Returns whether there is another entry available at a given index * - * @param compositeMetaData the buffer that will hold all composite metadata information. - * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. - * @param customMimeType the custom mime type to encode. - * @param metadata the metadata value to encode. + * @param compositeMetadata the buffer to inspect + * @param entryIndex the index to check at + * @return whether there is another entry available at a given index */ - // see #encodeMetadataHeader(ByteBufAllocator, String, int) - public static void encodeAndAddMetadata( - CompositeByteBuf compositeMetaData, - ByteBufAllocator allocator, - String customMimeType, - ByteBuf metadata) { - compositeMetaData.addComponents( - true, encodeMetadataHeader(allocator, customMimeType, metadata.readableBytes()), metadata); + public static boolean hasEntry(ByteBuf compositeMetadata, int entryIndex) { + return compositeMetadata.writerIndex() - entryIndex > 0; } /** - * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf - * buffer}. + * Returns whether the header represents a well-known MIME type. * - * @param compositeMetaData the buffer that will hold all composite metadata information. - * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. - * @param knownMimeType the {@link WellKnownMimeType} to encode. - * @param metadata the metadata value to encode. + * @param header the header to inspect + * @return whether the header represents a well-known MIME type */ - // see #encodeMetadataHeader(ByteBufAllocator, byte, int) - public static void encodeAndAddMetadata( - CompositeByteBuf compositeMetaData, - ByteBufAllocator allocator, - WellKnownMimeType knownMimeType, - ByteBuf metadata) { - compositeMetaData.addComponents( - true, - encodeMetadataHeader(allocator, knownMimeType.getIdentifier(), metadata.readableBytes()), - metadata); + public static boolean isWellKnownMimeType(ByteBuf header) { + return header.readableBytes() == 1; } /** @@ -357,4 +312,72 @@ static void encodeAndAddMetadata( encodeMetadataHeader(allocator, unknownCompressedMimeType, metadata.readableBytes()), metadata); } + + /** + * Encode a custom mime type and a metadata value length into a newly allocated {@link ByteBuf}. + * + *

This larger representation encodes the mime type representation's length on a single byte, + * then the representation itself, then the unsigned metadata value length on 3 additional bytes. + * + * @param allocator the {@link ByteBufAllocator} to use to create the buffer. + * @param customMime a custom mime type to encode. + * @param metadataLength the metadata length to append to the buffer as an unsigned 24 bits + * integer. + * @return the encoded mime and metadata length information + */ + static ByteBuf encodeMetadataHeader( + ByteBufAllocator allocator, String customMime, int metadataLength) { + ByteBuf metadataHeader = allocator.buffer(4 + customMime.length()); + // reserve 1 byte for the customMime length + int writerIndexInitial = metadataHeader.writerIndex(); + metadataHeader.writerIndex(writerIndexInitial + 1); + + // write the custom mime in UTF8 but validate it is all ASCII-compatible + // (which produces the right result since ASCII chars are still encoded on 1 byte in UTF8) + int customMimeLength = ByteBufUtil.writeUtf8(metadataHeader, customMime); + if (!ByteBufUtil.isText(metadataHeader, CharsetUtil.US_ASCII)) { + metadataHeader.release(); + throw new IllegalArgumentException("custom mime type must be US_ASCII characters only"); + } + if (customMimeLength < 1 || customMimeLength > 128) { + metadataHeader.release(); + throw new IllegalArgumentException( + "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + metadataHeader.markWriterIndex(); + + // go back to beginning and write the length + // encoded length is one less than actual length, since 0 is never a valid length, which gives + // wider representation range + metadataHeader.writerIndex(writerIndexInitial); + metadataHeader.writeByte(customMimeLength - 1); + + // go back to post-mime type and write the metadata content length + metadataHeader.resetWriterIndex(); + NumberUtils.encodeUnsignedMedium(metadataHeader, metadataLength); + + return metadataHeader; + } + + /** + * Encode a {@link WellKnownMimeType well known mime type} and a metadata value length into a + * newly allocated {@link ByteBuf}. + * + *

This compact representation encodes the mime type via its ID on a single byte, and the + * unsigned value length on 3 additional bytes. + * + * @param allocator the {@link ByteBufAllocator} to use to create the buffer. + * @param mimeType a byte identifier of a {@link WellKnownMimeType} to encode. + * @param metadataLength the metadata length to append to the buffer as an unsigned 24 bits + * integer. + * @return the encoded mime and metadata length information + */ + static ByteBuf encodeMetadataHeader( + ByteBufAllocator allocator, byte mimeType, int metadataLength) { + ByteBuf buffer = allocator.buffer(4, 4).writeByte(mimeType | STREAM_METADATA_KNOWN_MASK); + + NumberUtils.encodeUnsignedMedium(buffer, metadataLength); + + return buffer; + } } diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java index 20a6c741a..9ecaf0859 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java @@ -1,3 +1,19 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed 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 io.rsocket.metadata; import java.util.Arrays; @@ -12,48 +28,51 @@ public enum WellKnownMimeType { UNPARSEABLE_MIME_TYPE("UNPARSEABLE_MIME_TYPE_DO_NOT_USE", (byte) -2), UNKNOWN_RESERVED_MIME_TYPE("UNKNOWN_YET_RESERVED_DO_NOT_USE", (byte) -1), - APPLICATION_AVRO("application/avro", (byte) 0), - APPLICATION_CBOR("application/cbor", (byte) 1), - APPLICATION_GRAPHQL("application/graphql", (byte) 2), - APPLICATION_GZIP("application/gzip", (byte) 3), - APPLICATION_JAVASCRIPT("application/javascript", (byte) 4), - APPLICATION_JSON("application/json", (byte) 5), - APPLICATION_OCTET_STREAM("application/octet-stream", (byte) 6), - APPLICATION_PDF("application/pdf", (byte) 7), - APPLICATION_THRIFT("application/vnd.apache.thrift.binary", (byte) 8), - APPLICATION_PROTOBUF("application/vnd.google.protobuf", (byte) 9), - APPLICATION_XML("application/xml", (byte) 10), - APPLICATION_ZIP("application/zip", (byte) 11), - AUDIO_AAC("audio/aac", (byte) 12), - AUDIO_MP3("audio/mp3", (byte) 13), - AUDIO_MP4("audio/mp4", (byte) 14), - AUDIO_MPEG3("audio/mpeg3", (byte) 15), - AUDIO_MPEG("audio/mpeg", (byte) 16), - AUDIO_OGG("audio/ogg", (byte) 17), - AUDIO_OPUS("audio/opus", (byte) 18), - AUDIO_VORBIS("audio/vorbis", (byte) 19), - IMAGE_BMP("image/bmp", (byte) 20), - IMAGE_GIG("image/gif", (byte) 21), - IMAGE_HEIC_SEQUENCE("image/heic-sequence", (byte) 22), - IMAGE_HEIC("image/heic", (byte) 23), - IMAGE_HEIF_SEQUENCE("image/heif-sequence", (byte) 24), - IMAGE_HEIF("image/heif", (byte) 25), - IMAGE_JPEG("image/jpeg", (byte) 26), - IMAGE_PNG("image/png", (byte) 27), - IMAGE_TIFF("image/tiff", (byte) 28), - MULTIPART_MIXED("multipart/mixed", (byte) 29), - TEXT_CSS("text/css", (byte) 30), - TEXT_CSV("text/csv", (byte) 31), - TEXT_HTML("text/html", (byte) 32), - TEXT_PLAIN("text/plain", (byte) 33), - TEXT_XML("text/xml", (byte) 34), - VIDEO_H264("video/H264", (byte) 35), - VIDEO_H265("video/H265", (byte) 36), - VIDEO_VP8("video/VP8", (byte) 37), + + APPLICATION_AVRO("application/avro", (byte) 0x00), + APPLICATION_CBOR("application/cbor", (byte) 0x01), + APPLICATION_GRAPHQL("application/graphql", (byte) 0x02), + APPLICATION_GZIP("application/gzip", (byte) 0x03), + APPLICATION_JAVASCRIPT("application/javascript", (byte) 0x04), + APPLICATION_JSON("application/json", (byte) 0x05), + APPLICATION_OCTET_STREAM("application/octet-stream", (byte) 0x06), + APPLICATION_PDF("application/pdf", (byte) 0x07), + APPLICATION_THRIFT("application/vnd.apache.thrift.binary", (byte) 0x08), + APPLICATION_PROTOBUF("application/vnd.google.protobuf", (byte) 0x09), + APPLICATION_XML("application/xml", (byte) 0x0A), + APPLICATION_ZIP("application/zip", (byte) 0x0B), + AUDIO_AAC("audio/aac", (byte) 0x0C), + AUDIO_MP3("audio/mp3", (byte) 0x0D), + AUDIO_MP4("audio/mp4", (byte) 0x0E), + AUDIO_MPEG3("audio/mpeg3", (byte) 0x0F), + AUDIO_MPEG("audio/mpeg", (byte) 0x10), + AUDIO_OGG("audio/ogg", (byte) 0x11), + AUDIO_OPUS("audio/opus", (byte) 0x12), + AUDIO_VORBIS("audio/vorbis", (byte) 0x13), + IMAGE_BMP("image/bmp", (byte) 0x14), + IMAGE_GIG("image/gif", (byte) 0x15), + IMAGE_HEIC_SEQUENCE("image/heic-sequence", (byte) 0x16), + IMAGE_HEIC("image/heic", (byte) 0x17), + IMAGE_HEIF_SEQUENCE("image/heif-sequence", (byte) 0x18), + IMAGE_HEIF("image/heif", (byte) 0x19), + IMAGE_JPEG("image/jpeg", (byte) 0x1A), + IMAGE_PNG("image/png", (byte) 0x1B), + IMAGE_TIFF("image/tiff", (byte) 0x1C), + MULTIPART_MIXED("multipart/mixed", (byte) 0x1D), + TEXT_CSS("text/css", (byte) 0x1E), + TEXT_CSV("text/csv", (byte) 0x1F), + TEXT_HTML("text/html", (byte) 0x20), + TEXT_PLAIN("text/plain", (byte) 0x21), + TEXT_XML("text/xml", (byte) 0x22), + VIDEO_H264("video/H264", (byte) 0x23), + VIDEO_H265("video/H265", (byte) 0x24), + VIDEO_VP8("video/VP8", (byte) 0x25), + // ... reserved for future use ... - MESSAGE_RSOCKET_TRACING_ZIPKIN("message/x.rsocket.tracing-zipkin.v0", (byte) 125), - MESSAGE_RSOCKET_ROUTING("message/x.rsocket.routing.v0", (byte) 126), - MESSAGE_RSOCKET_COMPOSITE_METADATA("message/x.rsocket.composite-metadata.v0", (byte) 127); + + MESSAGE_RSOCKET_TRACING_ZIPKIN("message/x.rsocket.tracing-zipkin.v0", (byte) 0x7D), + MESSAGE_RSOCKET_ROUTING("message/x.rsocket.routing.v0", (byte) 0x7E), + MESSAGE_RSOCKET_COMPOSITE_METADATA("message/x.rsocket.composite-metadata.v0", (byte) 0x7F); static final WellKnownMimeType[] TYPES_BY_MIME_ID; static final Map TYPES_BY_MIME_STRING; @@ -68,36 +87,38 @@ public enum WellKnownMimeType { for (WellKnownMimeType value : values()) { if (value.getIdentifier() >= 0) { TYPES_BY_MIME_ID[value.getIdentifier()] = value; - TYPES_BY_MIME_STRING.put(value.getMime(), value); + TYPES_BY_MIME_STRING.put(value.getString(), value); } } } - private final String str; private final byte identifier; + private final String str; WellKnownMimeType(String str, byte identifier) { this.str = str; this.identifier = identifier; } - /** @return the byte identifier of the mime type, guaranteed to be positive or zero. */ - public byte getIdentifier() { - return identifier; - } - /** - * @return the mime type represented as a {@link String}, which is made of US_ASCII compatible - * characters only + * Find the {@link WellKnownMimeType} for the given identifier (as an {@code int}). Valid + * identifiers are defined to be integers between 0 and 127, inclusive. Identifiers outside of + * this range will produce the {@link #UNPARSEABLE_MIME_TYPE}. Additionally, some identifiers in + * that range are still only reserved and don't have a type associated yet: this method returns + * the {@link #UNKNOWN_RESERVED_MIME_TYPE} when passing such an identifier, which lets call sites + * potentially detect this and keep the original representation when transmitting the associated + * metadata buffer. + * + * @param id the looked up identifier + * @return the {@link WellKnownMimeType}, or {@link #UNKNOWN_RESERVED_MIME_TYPE} if the id is out + * of the specification's range, or {@link #UNKNOWN_RESERVED_MIME_TYPE} if the id is one that + * is merely reserved but unknown to this implementation. */ - public String getMime() { - return str; - } - - /** @see #getMime() */ - @Override - public String toString() { - return str; + public static WellKnownMimeType fromIdentifier(int id) { + if (id < 0x00 || id > 0x7F) { + return UNPARSEABLE_MIME_TYPE; + } + return TYPES_BY_MIME_ID[id]; } /** @@ -109,7 +130,7 @@ public String toString() { * @return the matching {@link WellKnownMimeType}, or {@link #UNPARSEABLE_MIME_TYPE} if none * matches */ - public static WellKnownMimeType fromMimeType(String mimeType) { + public static WellKnownMimeType fromString(String mimeType) { if (mimeType == null) throw new IllegalArgumentException("type must be non-null"); // force UNPARSEABLE if by chance UNKNOWN_RESERVED_MIME_TYPE's text has been used @@ -120,23 +141,22 @@ public static WellKnownMimeType fromMimeType(String mimeType) { return TYPES_BY_MIME_STRING.getOrDefault(mimeType, UNPARSEABLE_MIME_TYPE); } + /** @return the byte identifier of the mime type, guaranteed to be positive or zero. */ + public byte getIdentifier() { + return identifier; + } + /** - * Find the {@link WellKnownMimeType} for the given ID (as an int). Valid IDs are defined to be - * integers between 0 and 127, inclusive. IDs outside of this range will produce the {@link - * #UNPARSEABLE_MIME_TYPE}. Additionally, some IDs in that range are still only reserved and don't - * have a type associated yet: this method returns the {@link #UNKNOWN_RESERVED_MIME_TYPE} when - * passing such a ID, which lets call sites potentially detect this and keep the original - * representation when transmitting the associated metadata buffer. - * - * @param id the looked up id - * @return the {@link WellKnownMimeType}, or {@link #UNKNOWN_RESERVED_MIME_TYPE} if the id is out - * of the specification's range, or {@link #UNKNOWN_RESERVED_MIME_TYPE} if the id is one that - * is merely reserved but unknown to this implementation. + * @return the mime type represented as a {@link String}, which is made of US_ASCII compatible + * characters only */ - public static WellKnownMimeType fromId(int id) { - if (id < 0 || id > 127) { - return UNPARSEABLE_MIME_TYPE; - } - return TYPES_BY_MIME_ID[id]; + public String getString() { + return str; + } + + /** @see #getString() */ + @Override + public String toString() { + return str; } } diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java index 1139b2a63..1a22e9e23 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java @@ -1,10 +1,33 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed 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 io.rsocket.metadata; -import static io.rsocket.metadata.CompositeMetadataFlyweight.*; +import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeAndContentBuffersSlices; +import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeIdFromMimeBuffer; +import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import io.netty.buffer.*; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.util.CharsetUtil; import io.rsocket.test.util.ByteBufUtils; import io.rsocket.util.NumberUtils; @@ -12,6 +35,10 @@ class CompositeMetadataFlyweightTest { + static String byteToBitsString(byte b) { + return String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0'); + } + static String toHeaderBits(ByteBuf encoded) { encoded.markReaderIndex(); byte headerByte = encoded.readByte(); @@ -19,125 +46,41 @@ static String toHeaderBits(ByteBuf encoded) { encoded.resetReaderIndex(); return byteAsString; } - - static String byteToBitsString(byte b) { - return String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0'); - } // ==== @Test - void knownMimeHeaderZero_avro() { - WellKnownMimeType mime = WellKnownMimeType.APPLICATION_AVRO; - assertThat(mime.getIdentifier()) - .as("smoke test AVRO unsigned 7 bits representation") - .isEqualTo((byte) 0) - .isEqualTo((byte) 0b00000000); - ByteBuf encoded = - CompositeMetadataFlyweight.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); - - assertThat(toHeaderBits(encoded)) - .startsWith("1") - .isEqualTo("10000000") - .isEqualTo(byteToBitsString(mime.getIdentifier()).replaceFirst("0", "1")); - - final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); - assertThat(byteBufs).hasSize(2).doesNotContainNull(); - - ByteBuf header = byteBufs[0]; - ByteBuf content = byteBufs[1]; - header.markReaderIndex(); - - assertThat(header.readableBytes()).as("metadata header size").isOne(); - - assertThat(byteToBitsString(header.readByte())) - .as("header bit representation") - .isEqualTo("10000000"); - - header.resetReaderIndex(); - assertThat(decodeMimeIdFromMimeBuffer(header)) - .as("decoded mime id") - .isEqualTo(mime.getIdentifier()); - - assertThat(content.readableBytes()).as("no metadata content").isZero(); - } - - @Test - void knownMimeHeader127_compositeMetadata() { - WellKnownMimeType mime = WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA; - assertThat(mime.getIdentifier()) - .as("smoke test COMPOSITE unsigned 7 bits representation") - .isEqualTo((byte) 127) - .isEqualTo((byte) 0b01111111); - ByteBuf encoded = - CompositeMetadataFlyweight.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); - - assertThat(toHeaderBits(encoded)) - .startsWith("1") - .isEqualTo("11111111") - .isEqualTo(byteToBitsString(mime.getIdentifier()).replaceFirst("0", "1")); - - final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); - assertThat(byteBufs).hasSize(2).doesNotContainNull(); - - ByteBuf header = byteBufs[0]; - ByteBuf content = byteBufs[1]; - header.markReaderIndex(); - - assertThat(header.readableBytes()).as("metadata header size").isOne(); - - assertThat(byteToBitsString(header.readByte())) - .as("header bit representation") - .isEqualTo("11111111"); - - header.resetReaderIndex(); - assertThat(decodeMimeIdFromMimeBuffer(header)) - .as("decoded mime id") - .isEqualTo(mime.getIdentifier()); + void customMimeHeaderLatin1_encodingFails() { + String mimeNotAscii = "mime/typé"; - assertThat(content.readableBytes()).as("no metadata content").isZero(); + assertThatIllegalArgumentException() + .isThrownBy( + () -> + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mimeNotAscii, 0)) + .withMessage("custom mime type must be US_ASCII characters only"); } @Test - void knownMimeHeader120_reserved() { - byte mime = (byte) 120; - ByteBuf encoded = - CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime, 0); - - assertThat(mime) - .as("smoke test RESERVED_120 unsigned 7 bits representation") - .isEqualTo((byte) 0b01111000); - - assertThat(toHeaderBits(encoded)).startsWith("1").isEqualTo("11111000"); - - final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); - assertThat(byteBufs).hasSize(2).doesNotContainNull(); - - ByteBuf header = byteBufs[0]; - ByteBuf content = byteBufs[1]; - header.markReaderIndex(); - - assertThat(header.readableBytes()).as("metadata header size").isOne(); - - assertThat(byteToBitsString(header.readByte())) - .as("header bit representation") - .isEqualTo("11111000"); - - header.resetReaderIndex(); - assertThat(decodeMimeIdFromMimeBuffer(header)).as("decoded mime id").isEqualTo(mime); - - assertThat(content.readableBytes()).as("no metadata content").isZero(); + void customMimeHeaderLength0_encodingFails() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "", 0)) + .withMessage( + "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); } @Test - void customMimeHeaderLengthOne() { - String mimeString = "w"; + void customMimeHeaderLength127() { + StringBuilder builder = new StringBuilder(127); + for (int i = 0; i < 127; i++) { + builder.append('a'); + } + String mimeString = builder.toString(); ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); // remember actual length = encoded length + 1 - assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000000"); + assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111110"); final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); assertThat(byteBufs).hasSize(2).doesNotContainNull(); @@ -148,9 +91,11 @@ void customMimeHeaderLengthOne() { assertThat(header.readableBytes()).as("metadata header size").isGreaterThan(1); - assertThat((int) header.readByte()).as("mime length").isZero(); // encoded as actual length - 1 + assertThat((int) header.readByte()) + .as("mime length") + .isEqualTo(127 - 1); // encoded as actual length - 1 - assertThat(header.readCharSequence(1, CharsetUtil.US_ASCII)) + assertThat(header.readCharSequence(127, CharsetUtil.US_ASCII)) .as("mime string") .hasToString(mimeString); @@ -163,13 +108,17 @@ void customMimeHeaderLengthOne() { } @Test - void customMimeHeaderLengthTwo() { - String mimeString = "ww"; + void customMimeHeaderLength128() { + StringBuilder builder = new StringBuilder(128); + for (int i = 0; i < 128; i++) { + builder.append('a'); + } + String mimeString = builder.toString(); ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); // remember actual length = encoded length + 1 - assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000001"); + assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111111"); final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); assertThat(byteBufs).hasSize(2).doesNotContainNull(); @@ -182,9 +131,9 @@ void customMimeHeaderLengthTwo() { assertThat((int) header.readByte()) .as("mime length") - .isEqualTo(2 - 1); // encoded as actual length - 1 + .isEqualTo(128 - 1); // encoded as actual length - 1 - assertThat(header.readCharSequence(2, CharsetUtil.US_ASCII)) + assertThat(header.readCharSequence(128, CharsetUtil.US_ASCII)) .as("mime string") .hasToString(mimeString); @@ -197,17 +146,29 @@ void customMimeHeaderLengthTwo() { } @Test - void customMimeHeaderLength127() { - StringBuilder builder = new StringBuilder(127); - for (int i = 0; i < 127; i++) { + void customMimeHeaderLength129_encodingFails() { + StringBuilder builder = new StringBuilder(129); + for (int i = 0; i < 129; i++) { builder.append('a'); } - String mimeString = builder.toString(); + + assertThatIllegalArgumentException() + .isThrownBy( + () -> + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, builder.toString(), 0)) + .withMessage( + "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + + @Test + void customMimeHeaderLengthOne() { + String mimeString = "w"; ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); // remember actual length = encoded length + 1 - assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111110"); + assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000000"); final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); assertThat(byteBufs).hasSize(2).doesNotContainNull(); @@ -218,11 +179,9 @@ void customMimeHeaderLength127() { assertThat(header.readableBytes()).as("metadata header size").isGreaterThan(1); - assertThat((int) header.readByte()) - .as("mime length") - .isEqualTo(127 - 1); // encoded as actual length - 1 + assertThat((int) header.readByte()).as("mime length").isZero(); // encoded as actual length - 1 - assertThat(header.readCharSequence(127, CharsetUtil.US_ASCII)) + assertThat(header.readCharSequence(1, CharsetUtil.US_ASCII)) .as("mime string") .hasToString(mimeString); @@ -235,17 +194,13 @@ void customMimeHeaderLength127() { } @Test - void customMimeHeaderLength128() { - StringBuilder builder = new StringBuilder(128); - for (int i = 0; i < 128; i++) { - builder.append('a'); - } - String mimeString = builder.toString(); + void customMimeHeaderLengthTwo() { + String mimeString = "ww"; ByteBuf encoded = CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); // remember actual length = encoded length + 1 - assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111111"); + assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000001"); final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); assertThat(byteBufs).hasSize(2).doesNotContainNull(); @@ -258,9 +213,9 @@ void customMimeHeaderLength128() { assertThat((int) header.readByte()) .as("mime length") - .isEqualTo(128 - 1); // encoded as actual length - 1 + .isEqualTo(2 - 1); // encoded as actual length - 1 - assertThat(header.readCharSequence(128, CharsetUtil.US_ASCII)) + assertThat(header.readCharSequence(2, CharsetUtil.US_ASCII)) .as("mime string") .hasToString(mimeString); @@ -272,34 +227,6 @@ void customMimeHeaderLength128() { assertThat(content.readableBytes()).as("no metadata content").isZero(); } - @Test - void customMimeHeaderLength129_encodingFails() { - StringBuilder builder = new StringBuilder(129); - for (int i = 0; i < 129; i++) { - builder.append('a'); - } - - assertThatIllegalArgumentException() - .isThrownBy( - () -> - CompositeMetadataFlyweight.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, builder.toString(), 0)) - .withMessage( - "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); - } - - @Test - void customMimeHeaderLatin1_encodingFails() { - String mimeNotAscii = "mime/typé"; - - assertThatIllegalArgumentException() - .isThrownBy( - () -> - CompositeMetadataFlyweight.encodeMetadataHeader( - ByteBufAllocator.DEFAULT, mimeNotAscii, 0)) - .withMessage("custom mime type must be US_ASCII characters only"); - } - @Test void customMimeHeaderUtf8_encodingFails() { String mimeNotAscii = @@ -313,20 +240,11 @@ void customMimeHeaderUtf8_encodingFails() { } @Test - void customMimeHeaderLength0_encodingFails() { - assertThatIllegalArgumentException() - .isThrownBy( - () -> CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "", 0)) - .withMessage( - "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); - } - - @Test - void decodeEntryTooShortForMimeLength() { + void decodeEntryAtEndOfBuffer() { ByteBuf fakeEntry = Unpooled.buffer(); - fakeEntry.writeByte(120); - assertThat(decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)).isSameAs(METADATA_MALFORMED); + assertThatIllegalArgumentException() + .isThrownBy(() -> decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)); } @Test @@ -335,7 +253,8 @@ void decodeEntryHasNoContentLength() { fakeEntry.writeByte(0); fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); - assertThat(decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)).isSameAs(METADATA_MALFORMED); + assertThatIllegalStateException() + .isThrownBy(() -> decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)); } @Test @@ -346,47 +265,51 @@ void decodeEntryTooShortForContentLength() { NumberUtils.encodeUnsignedMedium(fakeEntry, 456); fakeEntry.writeChar('w'); - assertThat(decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)).isSameAs(METADATA_MALFORMED); + assertThatIllegalStateException() + .isThrownBy(() -> decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)); } @Test - void decodeEntryAtEndOfBuffer() { + void decodeEntryTooShortForMimeLength() { ByteBuf fakeEntry = Unpooled.buffer(); + fakeEntry.writeByte(120); - assertThat(decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)) - .isSameAs(METADATA_BUFFERS_DONE); + assertThatIllegalStateException() + .isThrownBy(() -> decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)); } @Test - void decodeIdMinusTwoWhenZeroByte() { - ByteBuf fakeIdBuffer = Unpooled.buffer(0); + void decodeIdMinusTwoWhenMoreThanOneByte() { + ByteBuf fakeIdBuffer = Unpooled.buffer(2); + fakeIdBuffer.writeInt(200); assertThat(decodeMimeIdFromMimeBuffer(fakeIdBuffer)) .isEqualTo((WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier())); } @Test - void decodeIdMinusTwoWhenMoreThanOneByte() { - ByteBuf fakeIdBuffer = Unpooled.buffer(2); - fakeIdBuffer.writeInt(200); + void decodeIdMinusTwoWhenZeroByte() { + ByteBuf fakeIdBuffer = Unpooled.buffer(0); assertThat(decodeMimeIdFromMimeBuffer(fakeIdBuffer)) .isEqualTo((WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier())); } @Test - void decodeStringNullIfLengthZero() { + void decodeStringNullIfLengthOne() { ByteBuf fakeTypeBuffer = Unpooled.buffer(2); + fakeTypeBuffer.writeByte(1); - assertThat(decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)).isNull(); + assertThatIllegalStateException() + .isThrownBy(() -> decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)); } @Test - void decodeStringNullIfLengthOne() { + void decodeStringNullIfLengthZero() { ByteBuf fakeTypeBuffer = Unpooled.buffer(2); - fakeTypeBuffer.writeByte(1); - assertThat(decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)).isNull(); + assertThatIllegalStateException() + .isThrownBy(() -> decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)); } @Test @@ -398,6 +321,19 @@ void decodeTypeSkipsFirstByte() { assertThat(decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)).hasToString("example"); } + @Test + void encodeMetadataCustomTypeDelegates() { + ByteBuf expected = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo", 2); + + CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataFlyweight.encodeAndAddMetadata( + test, ByteBufAllocator.DEFAULT, "foo", ByteBufUtils.getRandomByteBuf(2)); + + assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + } + @Test void encodeMetadataKnownTypeDelegates() { ByteBuf expected = @@ -431,16 +367,17 @@ void encodeMetadataReservedTypeDelegates() { } @Test - void encodeMetadataCustomTypeDelegates() { - ByteBuf expected = - CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo", 2); - - CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + void encodeTryCompressWithCompressableType() { + ByteBuf metadata = ByteBufUtils.getRandomByteBuf(2); + CompositeByteBuf target = UnpooledByteBufAllocator.DEFAULT.compositeBuffer(); - CompositeMetadataFlyweight.encodeAndAddMetadata( - test, ByteBufAllocator.DEFAULT, "foo", ByteBufUtils.getRandomByteBuf(2)); + CompositeMetadataFlyweight.encodeAndAddMetadataWithCompression( + target, + UnpooledByteBufAllocator.DEFAULT, + WellKnownMimeType.APPLICATION_AVRO.getString(), + metadata); - assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + assertThat(target.readableBytes()).as("readableBytes 1 + 3 + 2").isEqualTo(6); } @Test @@ -455,16 +392,136 @@ void encodeTryCompressWithCustomType() { } @Test - void encodeTryCompressWithCompressableType() { - ByteBuf metadata = ByteBufUtils.getRandomByteBuf(2); - CompositeByteBuf target = UnpooledByteBufAllocator.DEFAULT.compositeBuffer(); + void hasEntry() { + WellKnownMimeType mime = WellKnownMimeType.APPLICATION_AVRO; - CompositeMetadataFlyweight.encodeAndAddMetadataWithCompression( - target, - UnpooledByteBufAllocator.DEFAULT, - WellKnownMimeType.APPLICATION_AVRO.getMime(), - metadata); + CompositeByteBuf buffer = + Unpooled.compositeBuffer() + .addComponent( + true, + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0)) + .addComponent( + true, + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0)); - assertThat(target.readableBytes()).as("readableBytes 1 + 3 + 2").isEqualTo(6); + assertThat(CompositeMetadataFlyweight.hasEntry(buffer, 0)).isTrue(); + assertThat(CompositeMetadataFlyweight.hasEntry(buffer, 4)).isTrue(); + assertThat(CompositeMetadataFlyweight.hasEntry(buffer, 8)).isFalse(); + } + + @Test + void isWellKnownMimeType() { + ByteBuf wellKnown = Unpooled.buffer().writeByte(0); + assertThat(CompositeMetadataFlyweight.isWellKnownMimeType(wellKnown)).isTrue(); + + ByteBuf explicit = Unpooled.buffer().writeByte(2).writeChar('a'); + assertThat(CompositeMetadataFlyweight.isWellKnownMimeType(explicit)).isFalse(); + } + + @Test + void knownMimeHeader120_reserved() { + byte mime = (byte) 120; + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime, 0); + + assertThat(mime) + .as("smoke test RESERVED_120 unsigned 7 bits representation") + .isEqualTo((byte) 0b01111000); + + assertThat(toHeaderBits(encoded)).startsWith("1").isEqualTo("11111000"); + + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()).as("metadata header size").isOne(); + + assertThat(byteToBitsString(header.readByte())) + .as("header bit representation") + .isEqualTo("11111000"); + + header.resetReaderIndex(); + assertThat(decodeMimeIdFromMimeBuffer(header)).as("decoded mime id").isEqualTo(mime); + + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } + + @Test + void knownMimeHeader127_compositeMetadata() { + WellKnownMimeType mime = WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA; + assertThat(mime.getIdentifier()) + .as("smoke test COMPOSITE unsigned 7 bits representation") + .isEqualTo((byte) 127) + .isEqualTo((byte) 0b01111111); + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); + + assertThat(toHeaderBits(encoded)) + .startsWith("1") + .isEqualTo("11111111") + .isEqualTo(byteToBitsString(mime.getIdentifier()).replaceFirst("0", "1")); + + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()).as("metadata header size").isOne(); + + assertThat(byteToBitsString(header.readByte())) + .as("header bit representation") + .isEqualTo("11111111"); + + header.resetReaderIndex(); + assertThat(decodeMimeIdFromMimeBuffer(header)) + .as("decoded mime id") + .isEqualTo(mime.getIdentifier()); + + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } + + @Test + void knownMimeHeaderZero_avro() { + WellKnownMimeType mime = WellKnownMimeType.APPLICATION_AVRO; + assertThat(mime.getIdentifier()) + .as("smoke test AVRO unsigned 7 bits representation") + .isEqualTo((byte) 0) + .isEqualTo((byte) 0b00000000); + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); + + assertThat(toHeaderBits(encoded)) + .startsWith("1") + .isEqualTo("10000000") + .isEqualTo(byteToBitsString(mime.getIdentifier()).replaceFirst("0", "1")); + + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()).as("metadata header size").isOne(); + + assertThat(byteToBitsString(header.readByte())) + .as("header bit representation") + .isEqualTo("10000000"); + + header.resetReaderIndex(); + assertThat(decodeMimeIdFromMimeBuffer(header)) + .as("decoded mime id") + .isEqualTo(mime.getIdentifier()); + + assertThat(content.readableBytes()).as("no metadata content").isZero(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java index 2c46aa29a..cc00df7d4 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java @@ -1,40 +1,60 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed 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 io.rsocket.metadata; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; import io.netty.util.CharsetUtil; +import io.rsocket.metadata.CompositeMetadata.Entry; +import io.rsocket.metadata.CompositeMetadata.ReservedMimeTypeEntry; +import io.rsocket.metadata.CompositeMetadata.WellKnownMimeTypeEntry; import io.rsocket.test.util.ByteBufUtils; import io.rsocket.util.NumberUtils; -import java.util.NoSuchElementException; +import java.util.Iterator; import org.junit.jupiter.api.Test; class CompositeMetadataTest { @Test - void decodeEntryTooShortForMimeLength() { + void decodeEntryHasNoContentLength() { ByteBuf fakeEntry = Unpooled.buffer(); - fakeEntry.writeByte(120); + fakeEntry.writeByte(0); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); CompositeMetadata compositeMetadata = new CompositeMetadata(fakeEntry, false); - assertThatIllegalArgumentException() - .isThrownBy(compositeMetadata::decodeNext) - .withMessage("composite metadata entry buffer is too short to contain proper entry"); + assertThatIllegalStateException() + .isThrownBy(() -> compositeMetadata.iterator().next()) + .withMessage("metadata is malformed"); } @Test - void decodeEntryHasNoContentLength() { - ByteBuf fakeEntry = Unpooled.buffer(); - fakeEntry.writeByte(0); - fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); - CompositeMetadata compositeMetadata = new CompositeMetadata(fakeEntry, false); + void decodeEntryOnDoneBufferThrowsIllegalArgument() { + ByteBuf fakeBuffer = ByteBufUtils.getRandomByteBuf(0); + CompositeMetadata compositeMetadata = new CompositeMetadata(fakeBuffer, false); assertThatIllegalArgumentException() - .isThrownBy(compositeMetadata::decodeNext) - .withMessage("composite metadata entry buffer is too short to contain proper entry"); + .isThrownBy(() -> compositeMetadata.iterator().next()) + .withMessage("entry index 0 is larger than buffer size"); } @Test @@ -46,19 +66,20 @@ void decodeEntryTooShortForContentLength() { fakeEntry.writeChar('w'); CompositeMetadata compositeMetadata = new CompositeMetadata(fakeEntry, false); - assertThatIllegalArgumentException() - .isThrownBy(compositeMetadata::decodeNext) - .withMessage("composite metadata entry buffer is too short to contain proper entry"); + assertThatIllegalStateException() + .isThrownBy(() -> compositeMetadata.iterator().next()) + .withMessage("metadata is malformed"); } @Test - void decodeEntryOnDoneBufferThrowsNoSuchElement() { - ByteBuf fakeBuffer = ByteBufUtils.getRandomByteBuf(0); - CompositeMetadata compositeMetadata = new CompositeMetadata(fakeBuffer, false); + void decodeEntryTooShortForMimeLength() { + ByteBuf fakeEntry = Unpooled.buffer(); + fakeEntry.writeByte(120); + CompositeMetadata compositeMetadata = new CompositeMetadata(fakeEntry, false); - assertThatExceptionOfType(NoSuchElementException.class) - .isThrownBy(compositeMetadata::decodeNext) - .withMessage("composite metadata has no more entries"); + assertThatIllegalStateException() + .isThrownBy(() -> compositeMetadata.iterator().next()) + .withMessage("metadata is malformed"); } @Test @@ -79,7 +100,7 @@ void decodeThreeEntries() { // metadata 3: reserved but unknown byte reserved = 120; - assertThat(WellKnownMimeType.fromId(reserved)) + assertThat(WellKnownMimeType.fromIdentifier(reserved)) .as("ensure UNKNOWN RESERVED used in test") .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); ByteBuf metadata3 = Unpooled.buffer(); @@ -93,53 +114,44 @@ void decodeThreeEntries() { CompositeMetadataFlyweight.encodeAndAddMetadata( compositeMetadataBuffer, ByteBufAllocator.DEFAULT, reserved, metadata3); - CompositeMetadata compositeMetadata = new CompositeMetadata(compositeMetadataBuffer, true); + Iterator iterator = new CompositeMetadata(compositeMetadataBuffer, true).iterator(); - compositeMetadata.decodeNext(); - assertThat(compositeMetadata) + assertThat(iterator.next()) .as("entry1") .isNotNull() .satisfies( e -> - assertThat(e.getCurrentMimeType()) - .as("entry1 mime type") - .isEqualTo(mimeType1.getMime())) + assertThat(e.getMimeType()).as("entry1 mime type").isEqualTo(mimeType1.getString())) .satisfies( e -> - assertThat(e.getCurrentMimeId()) + assertThat(((WellKnownMimeTypeEntry) e).getType()) .as("entry1 mime id") - .isEqualTo((byte) mimeType1.getIdentifier())) + .isEqualTo(WellKnownMimeType.APPLICATION_PDF)) .satisfies( e -> - assertThat(e.getCurrentContent().toString(CharsetUtil.UTF_8)) + assertThat(e.getContent().toString(CharsetUtil.UTF_8)) .as("entry1 decoded") .isEqualTo("abcdefghijkl")); - compositeMetadata.decodeNext(); - assertThat(compositeMetadata) + assertThat(iterator.next()) .as("entry2") .isNotNull() + .satisfies(e -> assertThat(e.getMimeType()).as("entry2 mime type").isEqualTo(mimeType2)) .satisfies( - e -> assertThat(e.getCurrentMimeType()).as("entry2 mime type").isEqualTo(mimeType2)) - .satisfies(e -> assertThat(e.getCurrentMimeId()).as("entry2 mime id").isEqualTo((byte) -1)) - .satisfies( - e -> - assertThat(e.getCurrentContent()) - .as("entry2 decoded") - .isEqualByComparingTo(metadata2)); + e -> assertThat(e.getContent()).as("entry2 decoded").isEqualByComparingTo(metadata2)); - compositeMetadata.decodeNext(); - assertThat(compositeMetadata) + assertThat(iterator.next()) .as("entry3") .isNotNull() - .satisfies(e -> assertThat(e.getCurrentMimeType()).as("entry3 mime type").isNull()) - .satisfies(e -> assertThat(e.getCurrentMimeId()).as("entry3 mime id").isEqualTo(reserved)) + .satisfies(e -> assertThat(e.getMimeType()).as("entry3 mime type").isNull()) .satisfies( e -> - assertThat(e.getCurrentContent()) - .as("entry3 decoded") - .isEqualByComparingTo(metadata3)); + assertThat(((ReservedMimeTypeEntry) e).getType()) + .as("entry3 mime id") + .isEqualTo(reserved)) + .satisfies( + e -> assertThat(e.getContent()).as("entry3 decoded").isEqualByComparingTo(metadata3)); - assertThat(compositeMetadata.hasNext()).as("has no more than 3 entries").isFalse(); + assertThat(iterator.hasNext()).as("has no more than 3 entries").isFalse(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java index 0d36ef671..316aaf091 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java @@ -1,57 +1,74 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed 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 io.rsocket.metadata; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; class WellKnownMimeTypeTest { @Test - void fromIdMatchFromMimeType() { + void fromIdentifierGreaterThan127() { + assertThat(WellKnownMimeType.fromIdentifier(128)) + .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); + } + + @Test + void fromIdentifierMatchFromMimeType() { for (WellKnownMimeType mimeType : WellKnownMimeType.values()) { if (mimeType == WellKnownMimeType.UNPARSEABLE_MIME_TYPE || mimeType == WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE) { continue; } - assertThat(WellKnownMimeType.fromMimeType(mimeType.toString())) + assertThat(WellKnownMimeType.fromString(mimeType.toString())) .as("mimeType string for " + mimeType.name()) .isSameAs(mimeType); - assertThat(WellKnownMimeType.fromId(mimeType.getIdentifier())) + assertThat(WellKnownMimeType.fromIdentifier(mimeType.getIdentifier())) .as("mimeType ID for " + mimeType.name()) .isSameAs(mimeType); } } @Test - void fromIdNegative() { - assertThat(WellKnownMimeType.fromId(-1)) - .isSameAs(WellKnownMimeType.fromId(-2)) - .isSameAs(WellKnownMimeType.fromId(-12)) + void fromIdentifierNegative() { + assertThat(WellKnownMimeType.fromIdentifier(-1)) + .isSameAs(WellKnownMimeType.fromIdentifier(-2)) + .isSameAs(WellKnownMimeType.fromIdentifier(-12)) .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); } @Test - void fromIdGreaterThan127() { - assertThat(WellKnownMimeType.fromId(128)).isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); - } - - @Test - void fromIdReserved() { - assertThat(WellKnownMimeType.fromId(120)) + void fromIdentifierReserved() { + assertThat(WellKnownMimeType.fromIdentifier(120)) .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); } @Test - void fromMimeTypeUnknown() { - assertThat(WellKnownMimeType.fromMimeType("foo/bar")) + void fromStringUnknown() { + assertThat(WellKnownMimeType.fromString("foo/bar")) .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); } @Test - void fromMimeTypeUnkwnowReservedStillReturnsUnparseable() { + void fromStringUnknownReservedStillReturnsUnparseable() { assertThat( - WellKnownMimeType.fromMimeType(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE.getMime())) + WellKnownMimeType.fromString(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE.getString())) .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); } } From dba2684bca388bc822bfa9225ed561dac9321df1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Tue, 11 Jun 2019 13:10:50 -0400 Subject: [PATCH 25/25] Apply googleFormat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Simon Baslé --- .../main/java/io/rsocket/metadata/CompositeMetadata.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java index 9dde3eba6..9eb349396 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java @@ -168,9 +168,8 @@ public ByteBuf getContent() { } /** - * {@inheritDoc} - * Since this entry represents a compressed id that couldn't be decoded, this - * is always {@code null}. + * {@inheritDoc} Since this entry represents a compressed id that couldn't be decoded, this is + * always {@code null}. */ @Override public String getMimeType() { @@ -178,8 +177,8 @@ public String getMimeType() { } /** - * Returns the reserved, but unknown {@link WellKnownMimeType} for this entry. - * Range is 0-127 (inclusive). + * Returns the reserved, but unknown {@link WellKnownMimeType} for this entry. Range is 0-127 + * (inclusive). * * @return the reserved, but unknown {@link WellKnownMimeType} for this entry */