From 8395a35f9c09f870be83224ada8fe1f943fac23c Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Sun, 11 Aug 2024 15:58:11 +0200 Subject: [PATCH 01/33] Add msgpack item serializer and validator --- .../msgpack/low/internal/ItemSerializer.scala | 188 ++++++++++++++++++ .../msgpack/low/internal/ItemValidator.scala | 158 +++++++++++++++ .../scala/fs2/data/msgpack/low/package.scala | 34 +++- .../fs2/data/msgpack/SerializerSpec.scala | 135 +++++++++++++ 4 files changed, 514 insertions(+), 1 deletion(-) create mode 100644 msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala create mode 100644 msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala create mode 100644 msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala new file mode 100644 index 00000000..1895f570 --- /dev/null +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala @@ -0,0 +1,188 @@ +/* + * Copyright 2024 fs2-data Project + * + * 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 fs2 +package data +package msgpack +package low +package internal + +import scodec.bits._ + +private[low] object ItemSerializer { + def compressed: MsgpackItem => ByteVector = { + case MsgpackItem.UnsignedInt(bytes) => + if (bytes.size <= 1) + ByteVector(Headers.Uint8).buffer ++ bytes.padLeft(1) + else if (bytes.size <= 2) + ByteVector(Headers.Uint16).buffer ++ bytes.padLeft(2) + else if (bytes.size <= 4) + ByteVector(Headers.Uint32).buffer ++ bytes.padLeft(4) + else + ByteVector(Headers.Uint64).buffer ++ bytes.padLeft(8) + + case MsgpackItem.SignedInt(bytes) => + if (bytes.size <= 1) + // positive fixint or negative fixint + if ((bytes & hex"7f") == bytes || (bytes & hex"c0") == hex"c0") + bytes.padLeft(1) + else + ByteVector(Headers.Int8).buffer ++ bytes.padLeft(1) + else if (bytes.size <= 2) + ByteVector(Headers.Int16).buffer ++ bytes.padLeft(2) + else if (bytes.size <= 4) + ByteVector(Headers.Int32).buffer ++ bytes.padLeft(4) + else + ByteVector(Headers.Int64).buffer ++ bytes.padLeft(8) + + case MsgpackItem.Float32(float) => + ByteVector(Headers.Float32).buffer ++ ByteVector.fromInt(java.lang.Float.floatToIntBits(float)) + + case MsgpackItem.Float64(double) => + ByteVector(Headers.Float64).buffer ++ ByteVector.fromLong(java.lang.Double.doubleToLongBits(double)) + + case MsgpackItem.Str(bytes) => + if (bytes.size <= 31) { + ByteVector(0xa0 | bytes.size).buffer ++ bytes + } else if (bytes.size <= Math.pow(2, 8) - 1) { + ByteVector(Headers.Str8).buffer ++ ByteVector(bytes.size) ++ bytes + } else if (bytes.size <= Math.pow(2, 16) - 1) { + val size = ByteVector.fromShort(bytes.size.toShort) + ByteVector(Headers.Str16).buffer ++ size ++ bytes + } else { + val size = ByteVector.fromInt(bytes.size.toInt) + ByteVector(Headers.Str32).buffer ++ size ++ bytes + } + + case MsgpackItem.Bin(bytes) => + if (bytes.size <= Math.pow(2, 8) - 1) { + ByteVector(Headers.Bin8).buffer ++ ByteVector(bytes.size) ++ bytes + } else if (bytes.size <= Math.pow(2, 16) - 1) { + val size = ByteVector.fromShort(bytes.size.toShort) + ByteVector(Headers.Bin16).buffer ++ size ++ bytes + } else { + val size = ByteVector.fromInt(bytes.size.toInt).padLeft(4) + ByteVector(Headers.Bin32).buffer ++ size ++ bytes + } + + case MsgpackItem.Array(size) => + if (size <= 15) + ByteVector(0x90 | size) + else if (size <= Math.pow(2, 16) - 1) + ByteVector(Headers.Array16).buffer ++ ByteVector(size).padLeft(2) + else + ByteVector(Headers.Array32).buffer ++ ByteVector(size).padLeft(4) + + case MsgpackItem.Map(size) => + if (size <= 15) + ByteVector(0x80 | size) + else if (size <= Math.pow(2, 16) - 1) + ByteVector(Headers.Map16).buffer ++ ByteVector(size).padLeft(2) + else + ByteVector(Headers.Map32).buffer ++ ByteVector(size).padLeft(4) + + case MsgpackItem.Extension(tpe, bytes) => + if (bytes.size <= 1) + (ByteVector(Headers.FixExt1).buffer :+ tpe) ++ bytes.padLeft(1) + else if (bytes.size <= 2) + (ByteVector(Headers.FixExt2).buffer :+ tpe) ++ bytes.padLeft(2) + else if (bytes.size <= 4) + (ByteVector(Headers.FixExt4).buffer :+ tpe) ++ bytes.padLeft(4) + else if (bytes.size <= 8) + (ByteVector(Headers.FixExt8).buffer :+ tpe) ++ bytes.padLeft(8) + else if (bytes.size <= 16) + (ByteVector(Headers.FixExt16).buffer :+ tpe) ++ bytes.padLeft(16) + else if (bytes.size <= Math.pow(2, 8) - 1) + (ByteVector(Headers.Ext8).buffer ++ ByteVector(bytes.size) :+ tpe) ++ bytes + else if (bytes.size <= Math.pow(2, 16) - 1) + (ByteVector(Headers.Ext16).buffer ++ ByteVector(bytes.size) :+ tpe) ++ bytes.padLeft(2) + else + (ByteVector(Headers.Ext32).buffer ++ ByteVector(bytes.size) :+ tpe) ++ bytes.padLeft(4) + + case MsgpackItem.Timestamp32(seconds) => + (ByteVector(Headers.FixExt4).buffer :+ Headers.Timestamp.toByte) ++ ByteVector.fromInt(seconds) + + case MsgpackItem.Timestamp64(combined) => + (ByteVector(Headers.FixExt8).buffer :+ Headers.Timestamp.toByte) ++ ByteVector.fromLong(combined) + + case MsgpackItem.Timestamp96(nanoseconds, seconds) => + val ns = ByteVector.fromInt(nanoseconds) + val s = ByteVector.fromLong(seconds) + (ByteVector(Headers.Ext8).buffer :+ Headers.Timestamp.toByte) ++ ns ++ s + + case MsgpackItem.Nil => + ByteVector(Headers.Nil) + + case MsgpackItem.False => + ByteVector(Headers.False) + + case MsgpackItem.True => + ByteVector(Headers.True) + } + + def fast: MsgpackItem => ByteVector = { + case item: MsgpackItem.UnsignedInt => + ByteVector(Headers.Uint64) ++ item.bytes.padLeft(8) + + case item: MsgpackItem.SignedInt => + ByteVector(Headers.Int64) ++ item.bytes.padLeft(8) + + case item: MsgpackItem.Float32 => + ByteVector.fromInt(java.lang.Float.floatToIntBits(item.v)) + + case item: MsgpackItem.Float64 => + ByteVector.fromLong(java.lang.Double.doubleToLongBits(item.v)) + + case item: MsgpackItem.Str => + val size = ByteVector.fromInt(item.bytes.size.toInt) + ByteVector(Headers.Str32) ++ size ++ item.bytes + + case item: MsgpackItem.Bin => + val size = ByteVector.fromInt(item.bytes.size.toInt) + ByteVector(Headers.Bin32) ++ size ++ item.bytes + + case item: MsgpackItem.Array => + ByteVector(Headers.Array32) ++ ByteVector.fromInt(item.size).padLeft(4) + + case item: MsgpackItem.Map => + ByteVector(Headers.Map32) ++ ByteVector.fromInt(item.size).padLeft(4) + + case item: MsgpackItem.Extension => + val size = ByteVector.fromInt(item.bytes.size.toInt) + val t = ByteVector(item.tpe) + ByteVector(Headers.Ext32) ++ size ++ t ++ item.bytes + + case item: MsgpackItem.Timestamp32 => + ByteVector(Headers.FixExt4) ++ hex"ff" ++ ByteVector.fromInt(item.seconds) + + case item: MsgpackItem.Timestamp64 => + ByteVector(Headers.FixExt8) ++ hex"ff" ++ ByteVector.fromLong(item.combined) + + case item: MsgpackItem.Timestamp96 => + val ns = ByteVector.fromInt(item.nanoseconds) + val s = ByteVector.fromLong(item.seconds) + ByteVector(Headers.Ext8) ++ hex"0c" ++ hex"ff" ++ ns ++ s + + case MsgpackItem.Nil => + ByteVector(Headers.Nil) + + case MsgpackItem.False => + ByteVector(Headers.False) + + case MsgpackItem.True => + ByteVector(Headers.True) + } +} diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala new file mode 100644 index 00000000..5aac1f2d --- /dev/null +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala @@ -0,0 +1,158 @@ +/* + * Copyright 2024 fs2-data Project + * + * 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 fs2 +package data +package msgpack +package low +package internal + +case class ValidationErrorAt(at: Long, msg: String) extends Error(s"at position ${at}: ${msg}") +case class ValidationError(msg: String) extends Exception(msg) + +private[low] object ItemValidator { + + case class Expect(n: Int, from: Long) { + def dec = Expect(n - 1, from) + } + + type ValidationContext = (Chunk[MsgpackItem], Int, Long, List[Expect]) + + def none[F[_]]: Pipe[F, MsgpackItem, MsgpackItem] = in => in + + def simple[F[_]](implicit F: RaiseThrowable[F]): Pipe[F, MsgpackItem, MsgpackItem] = { in => + /** Validates one item from a stream + */ + def step1(chunk: Chunk[MsgpackItem], idx: Int, position: Long): Pull[F, MsgpackItem, Option[Expect]] = + chunk(idx) match { + case MsgpackItem.UnsignedInt(bytes) => + if (bytes.size > 8) Pull.raiseError(new ValidationErrorAt(position, "Unsigned int exceeds 64 bits")) + else Pull.pure(None) + + case MsgpackItem.SignedInt(bytes) => + if (bytes.size > 8) Pull.raiseError(new ValidationErrorAt(position, "Signed int exceeds 64 bits")) + else Pull.pure(None) + + case MsgpackItem.Float32(_) => + Pull.pure(None) + + case MsgpackItem.Float64(_) => + Pull.pure(None) + + case MsgpackItem.Str(bytes) => + if (bytes.size > Math.pow(2, 32) - 1) + Pull.raiseError(new ValidationErrorAt(position, "String exceeds (2^32)-1 bytes")) + else + Pull.pure(None) + + case MsgpackItem.Bin(bytes) => + if (bytes.size > Math.pow(2, 32) - 1) + Pull.raiseError(new ValidationErrorAt(position, "Bin exceeds (2^32)-1 bytes")) + else + Pull.pure(None) + + case MsgpackItem.Array(size) => + if (size < 0) + Pull.raiseError(new ValidationErrorAt(position, s"Array has a negative size ${size}")) + else if (size == 0) + Pull.pure(None) + else + Pull.pure(Some(Expect(size, position))) + + case MsgpackItem.Map(size) => + if (size < 0) + Pull.raiseError(new ValidationErrorAt(position, s"Map has a negative size ${size}")) + else if (size == 0) + Pull.pure(None) + else + Pull.pure(Some(Expect(size * 2, position))) + + case MsgpackItem.Extension(_, bytes) => + if (bytes.size > Math.pow(2, 32) - 1) + Pull.raiseError(new ValidationErrorAt(position, "Extension data exceeds (2^32)-1 bytes")) + else + Pull.pure(None) + + case _: MsgpackItem.Timestamp32 => + Pull.pure(None) + + case item: MsgpackItem.Timestamp64 => + if (item.nanoseconds > 999999999) + Pull.raiseError( + new ValidationErrorAt(position, "Timestamp64 nanoseconds cannot be larger than '999999999'")) + else + Pull.pure(None) + + case MsgpackItem.Timestamp96(nanoseconds, _) => + if (nanoseconds > 999999999) + Pull.raiseError( + new ValidationErrorAt(position, "Timestamp96 nanoseconds cannot be larger than '999999999'")) + else + Pull.pure(None) + + case MsgpackItem.Nil => + Pull.pure(None) + + case MsgpackItem.True => + Pull.pure(None) + + case MsgpackItem.False => + Pull.pure(None) + } + + def stepChunk(chunk: Chunk[MsgpackItem], + idx: Int, + stream: Stream[F, MsgpackItem], + position: Long, + state: List[Expect]): Pull[F, MsgpackItem, ValidationContext] = { + if (idx >= chunk.size) + Pull.output(chunk).as((Chunk.empty, 0, position, state)) + else + step1(chunk, idx, position, state).flatMap { el => + val stateNew: List[Expect] = + if (state.isEmpty) + state + else if (state.head.n == 1) + state.tail + else + state.head.dec :: state.tail + + val prepended = el match { + case Some(x) => x :: stateNew + case None => stateNew + } + + stepChunk(chunk, idx + 1, stream, position + 1, prepended) + } + } + + def go(stream: Stream[F, MsgpackItem], idx: Int, position: Long, state: List[Expect]): Pull[F, MsgpackItem, Unit] = + stream.pull.uncons.flatMap { + case Some((chunk, stream)) => + stepChunk(chunk, idx, stream, position, state).flatMap { case (_, idx, position, state) => + go(stream, idx, position, state) + } + case None => + if (state.isEmpty) + Pull.done + else + Pull.raiseError(new ValidationError(s"Unexpected end of input (starting at ${state.head.from})")) + } + + go(in, 0, 0, List.empty).stream + } + +} diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala index 118e19df..2d512faf 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala @@ -18,11 +18,43 @@ package fs2 package data package msgpack -import low.internal.ItemParser +import low.internal.{ItemParser, ItemSerializer, ItemValidator} /** A low-level representation of the MessagePack format. */ package object low { def items[F[_]](implicit F: RaiseThrowable[F]): Pipe[F, Byte, MsgpackItem] = ItemParser.pipe[F] + + /** Alias for `bytes(compressed = true, validated = true)` + */ + def toBinary[F[_]: RaiseThrowable]: Pipe[F, MsgpackItem, Byte] = + bytes(true, true) + + def bytes[F[_]](compressed: Boolean, validated: Boolean)(implicit + F: RaiseThrowable[F]): Pipe[F, MsgpackItem, Byte] = { in => + in + .through { if (validated) ItemValidator.simple else ItemValidator.none } + .flatMap { x => + val bytes = + if (compressed) + ItemSerializer.compressed(x) + else + ItemSerializer.fast(x) + + /* Maximum size of a `ByteVector` is bigger than the one of a `Chunk` (Long vs Int). The `Chunk.byteVector` + * function returns `Chunk.empty` if it encounters a `ByteVector` that won't fit in a `Chunk`. We have to work + * around this behaviour and explicitly check the `ByteVector` size. + */ + if (bytes.size <= Int.MaxValue) { + Stream.chunk(Chunk.byteVector(bytes)) + } else { + val (lhs, rhs) = bytes.splitAt(Int.MaxValue) + Stream.chunk(Chunk.byteVector(lhs)) ++ Stream.chunk(Chunk.byteVector(rhs)) + } + } + } + + def validated[F[_]](implicit F: RaiseThrowable[F]): Pipe[F, MsgpackItem, MsgpackItem] = + ItemValidator.simple[F] } diff --git a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala new file mode 100644 index 00000000..3a826c8c --- /dev/null +++ b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala @@ -0,0 +1,135 @@ +/* + * Copyright 2024 fs2-data Project + * + * 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 fs2 +package data +package msgpack + +import weaver.SimpleIOSuite +import scodec.bits.* +import low.MsgpackItem +import cats.effect.* +import low.internal.ValidationErrorAt +import low.internal.ValidationError + +import java.nio.charset.StandardCharsets + +object SerializerSpec extends SimpleIOSuite { + test("MessagePack item serializer should correctly serialize all formats") { + val cases = List( + // positive fixint + (List(MsgpackItem.SignedInt(hex"7b")), hex"7b", hex"0xd3000000000000007b"), + // fixmap + (List(MsgpackItem.Map(1)), hex"81", hex"0xdf00000001"), + // fixarray + (List(MsgpackItem.Array(1)), hex"91", hex"0xdd00000001"), + // fixstr + (List(MsgpackItem.Str(ByteVector("foobar".getBytes(StandardCharsets.UTF_8)))), + hex"a6666f6f626172", + hex"0xdb00000006666f6f626172"), + // nil, false, true + (List(MsgpackItem.Nil, MsgpackItem.False, MsgpackItem.True), hex"c0c2c3", hex"c0c2c3"), + // bin8, bin16, bin32 + (List(MsgpackItem.Bin(hex"abc")), hex"c4020abc", hex"c6000000020abc"), + (List(MsgpackItem.Bin(hex"abc".padLeft(Math.pow(2, 8).toLong))), + ByteVector(Headers.Bin16) ++ hex"0100" ++ hex"0abc".padLeft(Math.pow(2, 8).toLong), + ByteVector(Headers.Bin32) ++ hex"00000100" ++ hex"0abc".padLeft(Math.pow(2, 8).toLong)), + (List(MsgpackItem.Bin(hex"abc".padLeft(Math.pow(2, 16).toLong))), + ByteVector(Headers.Bin32) ++ hex"00010000" ++ ByteVector.empty.padLeft(Math.pow(2, 16).toLong - 2) ++ hex"0abc", + ByteVector(Headers.Bin32) ++ hex"00010000" ++ ByteVector.empty.padLeft(Math.pow(2, 16).toLong - 2) ++ hex"0abc") + // ext8, ext16, ext32 + ) + + Stream + .emits(cases) + .evalMap { case (source, compressed, fast) => + for { + e1 <- + Stream + .emits(source) + .through(low.bytes(true, false)) + .compile + .fold(ByteVector.empty)(_ :+ _) + .map(expect.same(_, compressed)) + + e2 <- + Stream + .emits(source) + .through(low.bytes(false, false)) + .compile + .fold(ByteVector.empty)(_ :+ _) + .map(expect.same(_, fast)) + } yield e1 and e2 + } + .compile + .foldMonoid + } + + test("MessagePack item serializer should be fix point when optimizing for size") { + val cases = List( + hex"CB3FCB5A858793DD98", + hex"d6ffaabbccdd", + hex"81A77765622D61707083A7736572766C65749583AC736572766C65742D6E616D65A8636F666178434453AD736572766C65742D636C617373B86F72672E636F6661782E6364732E434453536572766C6574AA696E69742D706172616DDE002ABD636F6E666967476C6F73736172793A696E7374616C6C6174696F6E4174B05068696C6164656C706869612C205041B9636F6E666967476C6F73736172793A61646D696E456D61696CAD6B736D40706F626F782E636F6DB8636F6E666967476C6F73736172793A706F77657265644279A5436F666178BC636F6E666967476C6F73736172793A706F7765726564427949636F6EB12F696D616765732F636F6661782E676966B9636F6E666967476C6F73736172793A73746174696350617468AF2F636F6E74656E742F737461746963B674656D706C61746550726F636573736F72436C617373B96F72672E636F6661782E5779736977796754656D706C617465B374656D706C6174654C6F61646572436C617373BD6F72672E636F6661782E46696C657354656D706C6174654C6F61646572AC74656D706C61746550617468A974656D706C61746573B474656D706C6174654F7665727269646550617468A0B364656661756C744C69737454656D706C617465B06C69737454656D706C6174652E68746DB364656661756C7446696C6554656D706C617465B361727469636C6554656D706C6174652E68746DA67573654A5350C2AF6A73704C69737454656D706C617465B06C69737454656D706C6174652E6A7370AF6A737046696C6554656D706C617465B361727469636C6554656D706C6174652E6A7370B563616368655061636B61676554616773547261636BCCC8B563616368655061636B6167655461677353746F7265CCC8B763616368655061636B61676554616773526566726573683CB3636163686554656D706C61746573547261636B64B3636163686554656D706C6174657353746F726532B5636163686554656D706C61746573526566726573680FAF63616368655061676573547261636BCCC8AF6361636865506167657353746F726564B163616368655061676573526566726573680AB3636163686550616765734469727479526561640AB8736561726368456E67696E654C69737454656D706C617465B8666F72536561726368456E67696E65734C6973742E68746DB8736561726368456E67696E6546696C6554656D706C617465B4666F72536561726368456E67696E65732E68746DB4736561726368456E67696E65526F626F74734462B15745422D494E462F726F626F74732E6462AC7573654461746153746F7265C3AE6461746153746F7265436C617373B66F72672E636F6661782E53716C4461746153746F7265B07265646972656374696F6E436C617373B86F72672E636F6661782E53716C5265646972656374696F6EAD6461746153746F72654E616D65A5636F666178AF6461746153746F7265447269766572D92C636F6D2E6D6963726F736F66742E6A6462632E73716C7365727665722E53514C536572766572447269766572AC6461746153746F726555726CD93B6A6462633A6D6963726F736F66743A73716C7365727665723A2F2F4C4F43414C484F53543A313433333B44617461626173654E616D653D676F6F6EAD6461746153746F726555736572A27361B16461746153746F726550617373776F7264B26461746153746F7265546573745175657279B26461746153746F7265546573745175657279D922534554204E4F434F554E54204F4E3B73656C65637420746573743D2774657374273BB06461746153746F72654C6F6746696C65D9242F7573722F6C6F63616C2F746F6D6361742F6C6F67732F6461746173746F72652E6C6F67B26461746153746F7265496E6974436F6E6E730AB16461746153746F72654D6178436F6E6E7364B76461746153746F7265436F6E6E55736167654C696D697464B16461746153746F72654C6F674C6576656CA56465627567AC6D617855726C4C656E677468CD01F483AC736572766C65742D6E616D65AA636F666178456D61696CAD736572766C65742D636C617373BA6F72672E636F6661782E6364732E456D61696C536572766C6574AA696E69742D706172616D82A86D61696C486F7374A56D61696C31B06D61696C486F73744F76657272696465A56D61696C3282AC736572766C65742D6E616D65AA636F66617841646D696EAD736572766C65742D636C617373BA6F72672E636F6661782E6364732E41646D696E536572766C657482AC736572766C65742D6E616D65AB66696C65536572766C6574AD736572766C65742D636C617373B96F72672E636F6661782E6364732E46696C65536572766C657483AC736572766C65742D6E616D65AA636F666178546F6F6C73AD736572766C65742D636C617373BF6F72672E636F6661782E636D732E436F666178546F6F6C73536572766C6574AA696E69742D706172616D8DAC74656D706C61746550617468AF746F6F6C7374656D706C617465732FA36C6F6701AB6C6F674C6F636174696F6ED9252F7573722F6C6F63616C2F746F6D6361742F6C6F67732F436F666178546F6F6C732E6C6F67AA6C6F674D617853697A65A0A7646174614C6F6701AF646174614C6F674C6F636174696F6ED9222F7573722F6C6F63616C2F746F6D6361742F6C6F67732F646174614C6F672E6C6F67AE646174614C6F674D617853697A65A0AF72656D6F7665506167654361636865D9252F636F6E74656E742F61646D696E2F72656D6F76653F63616368653D70616765732669643DB372656D6F766554656D706C6174654361636865D9292F636F6E74656E742F61646D696E2F72656D6F76653F63616368653D74656D706C617465732669643DB266696C655472616E73666572466F6C646572D9342F7573722F6C6F63616C2F746F6D6361742F776562617070732F636F6E74656E742F66696C655472616E73666572466F6C646572AD6C6F6F6B496E436F6E7465787401AC61646D696E47726F7570494404AA62657461536572766572C3AF736572766C65742D6D617070696E6785A8636F666178434453A12FAA636F666178456D61696CB32F636F6661787574696C2F61656D61696C2F2AAA636F66617841646D696EA82F61646D696E2F2AAB66696C65536572766C6574A92F7374617469632F2AAA636F666178546F6F6C73A82F746F6F6C732F2AA67461676C696282AA7461676C69622D757269A9636F6661782E746C64AF7461676C69622D6C6F636174696F6EB72F5745422D494E462F746C64732F636F6661782E746C64" + ) + + Stream + .emits(cases) + .evalMap { hex => + Stream + .chunk(Chunk.byteVector(hex)) + .through(low.items) + .through(low.toBinary) + .compile + .toList + .map(x => expect.same(ByteVector(x), hex)) + } + .compile + .foldMonoid + } + + test("MessagePack item validator should raise for all checks") { + val cases = List( + List(MsgpackItem.UnsignedInt(hex"10000000000000000")) -> new ValidationErrorAt(0, "Unsigned int exceeds 64 bits"), + List(MsgpackItem.SignedInt(hex"10000000000000000")) -> new ValidationErrorAt(0, "Signed int exceeds 64 bits"), + + // TODO: Float32, Float64 + + List(MsgpackItem.Str(ByteVector.fill(Math.pow(2, 32).toLong)(1))) -> new ValidationErrorAt( + 0, + "String exceeds (2^32)-1 bytes"), + List(MsgpackItem.Bin(ByteVector.fill(Math.pow(2, 32).toLong)(1))) -> new ValidationErrorAt( + 0, + "Bin exceeds (2^32)-1 bytes"), + List(MsgpackItem.Array(2), MsgpackItem.True) -> new ValidationError("Unexpected end of input (starting at 0)"), + List(MsgpackItem.Map(1), MsgpackItem.Array(1), MsgpackItem.True) -> new ValidationError( + "Unexpected end of input (starting at 0)") + ) + + Stream + .emits(cases) + .evalMap { case (lhs, rhs) => + Stream + .emits(lhs) + .through(low.validated[IO]) + .compile + .toList + .map(x => failure(s"Expected error for item ${x}")) + .handleErrorWith(err => IO(expect.same(err, rhs))) + } + .compile + .foldMonoid + } +} From 52b48d7122a24a19779cd06073d5d23e5d1fc698 Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Tue, 13 Aug 2024 21:07:22 +0200 Subject: [PATCH 02/33] Fix one additional argument being passed --- .../scala/fs2/data/msgpack/low/internal/ItemValidator.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala index 5aac1f2d..d9402fee 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala @@ -121,7 +121,7 @@ private[low] object ItemValidator { if (idx >= chunk.size) Pull.output(chunk).as((Chunk.empty, 0, position, state)) else - step1(chunk, idx, position, state).flatMap { el => + step1(chunk, idx, position).flatMap { el => val stateNew: List[Expect] = if (state.isEmpty) state From 7b4246e6e72135719f510b1d5f15656b1952be68 Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Tue, 13 Aug 2024 21:08:29 +0200 Subject: [PATCH 03/33] Add tests to cover all fmts for msgpack serializer And fix issues found during testing --- .../msgpack/low/internal/ItemSerializer.scala | 70 +++++---- .../fs2/data/msgpack/SerializerSpec.scala | 134 +++++++++++++++--- 2 files changed, 157 insertions(+), 47 deletions(-) diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala index 1895f570..cd6a2473 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala @@ -56,9 +56,10 @@ private[low] object ItemSerializer { case MsgpackItem.Str(bytes) => if (bytes.size <= 31) { - ByteVector(0xa0 | bytes.size).buffer ++ bytes + ByteVector.fromByte((0xa0 | bytes.size).toByte).buffer ++ bytes } else if (bytes.size <= Math.pow(2, 8) - 1) { - ByteVector(Headers.Str8).buffer ++ ByteVector(bytes.size) ++ bytes + val size = ByteVector.fromByte(bytes.size.toByte) + ByteVector(Headers.Str8).buffer ++ size ++ bytes } else if (bytes.size <= Math.pow(2, 16) - 1) { val size = ByteVector.fromShort(bytes.size.toShort) ByteVector(Headers.Str16).buffer ++ size ++ bytes @@ -69,7 +70,8 @@ private[low] object ItemSerializer { case MsgpackItem.Bin(bytes) => if (bytes.size <= Math.pow(2, 8) - 1) { - ByteVector(Headers.Bin8).buffer ++ ByteVector(bytes.size) ++ bytes + val size = ByteVector.fromByte(bytes.size.toByte) + ByteVector(Headers.Bin8).buffer ++ size ++ bytes } else if (bytes.size <= Math.pow(2, 16) - 1) { val size = ByteVector.fromShort(bytes.size.toShort) ByteVector(Headers.Bin16).buffer ++ size ++ bytes @@ -79,38 +81,48 @@ private[low] object ItemSerializer { } case MsgpackItem.Array(size) => - if (size <= 15) - ByteVector(0x90 | size) - else if (size <= Math.pow(2, 16) - 1) - ByteVector(Headers.Array16).buffer ++ ByteVector(size).padLeft(2) - else - ByteVector(Headers.Array32).buffer ++ ByteVector(size).padLeft(4) + if (size <= 15) { + ByteVector.fromByte((0x90 | size).toByte) + } else if (size <= Math.pow(2, 16) - 1) { + val s = ByteVector.fromShort(size.toShort) + ByteVector(Headers.Array16).buffer ++ s + } else { + val s = ByteVector.fromInt(size) + ByteVector(Headers.Array32).buffer ++ s + } case MsgpackItem.Map(size) => - if (size <= 15) - ByteVector(0x80 | size) - else if (size <= Math.pow(2, 16) - 1) - ByteVector(Headers.Map16).buffer ++ ByteVector(size).padLeft(2) - else - ByteVector(Headers.Map32).buffer ++ ByteVector(size).padLeft(4) + if (size <= 15) { + ByteVector.fromByte((0x80 | size).toByte) + } else if (size <= Math.pow(2, 16) - 1) { + val s = ByteVector.fromShort(size.toShort) + ByteVector(Headers.Map16).buffer ++ s + } else { + val s = ByteVector.fromInt(size) + ByteVector(Headers.Map32).buffer ++ s + } case MsgpackItem.Extension(tpe, bytes) => - if (bytes.size <= 1) + if (bytes.size <= 1) { (ByteVector(Headers.FixExt1).buffer :+ tpe) ++ bytes.padLeft(1) - else if (bytes.size <= 2) + } else if (bytes.size <= 2) { (ByteVector(Headers.FixExt2).buffer :+ tpe) ++ bytes.padLeft(2) - else if (bytes.size <= 4) + } else if (bytes.size <= 4) { (ByteVector(Headers.FixExt4).buffer :+ tpe) ++ bytes.padLeft(4) - else if (bytes.size <= 8) + } else if (bytes.size <= 8) { (ByteVector(Headers.FixExt8).buffer :+ tpe) ++ bytes.padLeft(8) - else if (bytes.size <= 16) + } else if (bytes.size <= 16) { (ByteVector(Headers.FixExt16).buffer :+ tpe) ++ bytes.padLeft(16) - else if (bytes.size <= Math.pow(2, 8) - 1) - (ByteVector(Headers.Ext8).buffer ++ ByteVector(bytes.size) :+ tpe) ++ bytes - else if (bytes.size <= Math.pow(2, 16) - 1) - (ByteVector(Headers.Ext16).buffer ++ ByteVector(bytes.size) :+ tpe) ++ bytes.padLeft(2) - else - (ByteVector(Headers.Ext32).buffer ++ ByteVector(bytes.size) :+ tpe) ++ bytes.padLeft(4) + } else if (bytes.size <= Math.pow(2, 8) - 1) { + val size = ByteVector.fromByte(bytes.size.toByte) + (ByteVector(Headers.Ext8).buffer ++ size :+ tpe) ++ bytes + } else if (bytes.size <= Math.pow(2, 16) - 1) { + val size = ByteVector.fromShort(bytes.size.toShort) + (ByteVector(Headers.Ext16).buffer ++ size :+ tpe) ++ bytes + } else { + val size = ByteVector.fromInt(bytes.size.toInt) + (ByteVector(Headers.Ext32).buffer ++ size :+ tpe) ++ bytes + } case MsgpackItem.Timestamp32(seconds) => (ByteVector(Headers.FixExt4).buffer :+ Headers.Timestamp.toByte) ++ ByteVector.fromInt(seconds) @@ -121,7 +133,7 @@ private[low] object ItemSerializer { case MsgpackItem.Timestamp96(nanoseconds, seconds) => val ns = ByteVector.fromInt(nanoseconds) val s = ByteVector.fromLong(seconds) - (ByteVector(Headers.Ext8).buffer :+ Headers.Timestamp.toByte) ++ ns ++ s + (ByteVector(Headers.Ext8).buffer :+ 12 :+ Headers.Timestamp.toByte) ++ ns ++ s case MsgpackItem.Nil => ByteVector(Headers.Nil) @@ -141,10 +153,10 @@ private[low] object ItemSerializer { ByteVector(Headers.Int64) ++ item.bytes.padLeft(8) case item: MsgpackItem.Float32 => - ByteVector.fromInt(java.lang.Float.floatToIntBits(item.v)) + ByteVector(Headers.Float32) ++ ByteVector.fromInt(java.lang.Float.floatToIntBits(item.v)) case item: MsgpackItem.Float64 => - ByteVector.fromLong(java.lang.Double.doubleToLongBits(item.v)) + ByteVector(Headers.Float64) ++ ByteVector.fromLong(java.lang.Double.doubleToLongBits(item.v)) case item: MsgpackItem.Str => val size = ByteVector.fromInt(item.bytes.size.toInt) diff --git a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala index 3a826c8c..ba89f681 100644 --- a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala +++ b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala @@ -30,27 +30,121 @@ import java.nio.charset.StandardCharsets object SerializerSpec extends SimpleIOSuite { test("MessagePack item serializer should correctly serialize all formats") { val cases = List( + // nil, false, true + (List(MsgpackItem.Nil, MsgpackItem.False, MsgpackItem.True), hex"c0c2c3", hex"c0c2c3"), + // positive fixint (List(MsgpackItem.SignedInt(hex"7b")), hex"7b", hex"0xd3000000000000007b"), - // fixmap - (List(MsgpackItem.Map(1)), hex"81", hex"0xdf00000001"), - // fixarray - (List(MsgpackItem.Array(1)), hex"91", hex"0xdd00000001"), + // negative fixint + (List(MsgpackItem.SignedInt(hex"d6")), hex"d6", hex"0xd300000000000000d6"), + + // uint 8, uint 16, uint 32, uint 64 + (List(MsgpackItem.UnsignedInt(hex"ab")), hex"ccab", hex"cf00000000000000ab"), + (List(MsgpackItem.UnsignedInt(hex"abcd")), hex"cdabcd", hex"cf000000000000abcd"), + (List(MsgpackItem.UnsignedInt(hex"abcdef01")), hex"ceabcdef01", hex"cf00000000abcdef01"), + (List(MsgpackItem.UnsignedInt(hex"abcdef0123456789")), hex"cfabcdef0123456789", hex"cfabcdef0123456789"), + + // int 8, int 16, int 32, int 64 + (List(MsgpackItem.SignedInt(hex"80")), hex"d080", hex"d30000000000000080"), + (List(MsgpackItem.SignedInt(hex"80ab")), hex"d180ab", hex"d300000000000080ab"), + (List(MsgpackItem.SignedInt(hex"80abcdef")), hex"d280abcdef", hex"d30000000080abcdef"), + (List(MsgpackItem.SignedInt(hex"80abcddef0123456")), hex"d380abcddef0123456", hex"d380abcddef0123456"), + + // float 32, float 64 + (List(MsgpackItem.Float32(0.125F)), hex"ca3e000000", hex"ca3e000000"), + (List(MsgpackItem.Float64(0.125)), hex"cb3fc0000000000000", hex"cb3fc0000000000000"), + // fixstr - (List(MsgpackItem.Str(ByteVector("foobar".getBytes(StandardCharsets.UTF_8)))), - hex"a6666f6f626172", - hex"0xdb00000006666f6f626172"), - // nil, false, true - (List(MsgpackItem.Nil, MsgpackItem.False, MsgpackItem.True), hex"c0c2c3", hex"c0c2c3"), - // bin8, bin16, bin32 - (List(MsgpackItem.Bin(hex"abc")), hex"c4020abc", hex"c6000000020abc"), - (List(MsgpackItem.Bin(hex"abc".padLeft(Math.pow(2, 8).toLong))), - ByteVector(Headers.Bin16) ++ hex"0100" ++ hex"0abc".padLeft(Math.pow(2, 8).toLong), - ByteVector(Headers.Bin32) ++ hex"00000100" ++ hex"0abc".padLeft(Math.pow(2, 8).toLong)), - (List(MsgpackItem.Bin(hex"abc".padLeft(Math.pow(2, 16).toLong))), - ByteVector(Headers.Bin32) ++ hex"00010000" ++ ByteVector.empty.padLeft(Math.pow(2, 16).toLong - 2) ++ hex"0abc", - ByteVector(Headers.Bin32) ++ hex"00010000" ++ ByteVector.empty.padLeft(Math.pow(2, 16).toLong - 2) ++ hex"0abc") - // ext8, ext16, ext32 + (List(MsgpackItem.Str(ByteVector("abc".getBytes(StandardCharsets.UTF_8)))), + hex"a3616263", + hex"0xdb00000003616263"), + + // str 8 + (List(MsgpackItem.Str(ByteVector("abcd".repeat(8).getBytes(StandardCharsets.UTF_8)))), + hex"d920" ++ ByteVector("abcd".repeat(8).getBytes(StandardCharsets.UTF_8)), + hex"db00000020" ++ ByteVector("abcd".repeat(8).getBytes(StandardCharsets.UTF_8))), + + // str 16 + (List(MsgpackItem.Str(ByteVector("a".repeat(Math.pow(2, 8).toInt).getBytes(StandardCharsets.UTF_8)))), + hex"da0100" ++ ByteVector("a".repeat(Math.pow(2, 8).toInt).getBytes(StandardCharsets.UTF_8)), + hex"db00000100" ++ ByteVector("a".repeat(Math.pow(2, 8).toInt).getBytes(StandardCharsets.UTF_8))), + + // str 32 + (List(MsgpackItem.Str(ByteVector("a".repeat(Math.pow(2, 16).toInt).getBytes(StandardCharsets.UTF_8)))), + hex"db00010000" ++ ByteVector("a".repeat(Math.pow(2, 16).toInt).getBytes(StandardCharsets.UTF_8)), + hex"db00010000" ++ ByteVector("a".repeat(Math.pow(2, 16).toInt).getBytes(StandardCharsets.UTF_8))), + + // bin 8 + (List(MsgpackItem.Bin(ByteVector("abcd".repeat(8).getBytes(StandardCharsets.UTF_8)))), + hex"c420" ++ ByteVector("abcd".repeat(8).getBytes(StandardCharsets.UTF_8)), + hex"c600000020" ++ ByteVector("abcd".repeat(8).getBytes(StandardCharsets.UTF_8))), + + // bin 16 + (List(MsgpackItem.Bin(ByteVector("a".repeat(Math.pow(2, 8).toInt).getBytes(StandardCharsets.UTF_8)))), + hex"c50100" ++ ByteVector("a".repeat(Math.pow(2, 8).toInt).getBytes(StandardCharsets.UTF_8)), + hex"c600000100" ++ ByteVector("a".repeat(Math.pow(2, 8).toInt).getBytes(StandardCharsets.UTF_8))), + + // bin 32 + (List(MsgpackItem.Bin(ByteVector("a".repeat(Math.pow(2, 16).toInt).getBytes(StandardCharsets.UTF_8)))), + hex"c600010000" ++ ByteVector("a".repeat(Math.pow(2, 16).toInt).getBytes(StandardCharsets.UTF_8)), + hex"c600010000" ++ ByteVector("a".repeat(Math.pow(2, 16).toInt).getBytes(StandardCharsets.UTF_8))), + + // fixarray + (List(MsgpackItem.Array(0)), hex"90", hex"dd00000000"), + (List(MsgpackItem.Array(1)), hex"91", hex"dd00000001"), + // array 16 + (List(MsgpackItem.Array(16)), hex"dc0010", hex"dd00000010"), + // array 32 + (List(MsgpackItem.Array(Math.pow(2, 16).toInt)), hex"dd00010000", hex"dd00010000"), + + // fixmap + (List(MsgpackItem.Map(0)), hex"80", hex"df00000000"), + (List(MsgpackItem.Map(1)), hex"81", hex"df00000001"), + // map 16 + (List(MsgpackItem.Map(16)), hex"de0010", hex"df00000010"), + // map 32 + (List(MsgpackItem.Map(Math.pow(2, 16).toInt)), hex"df00010000", hex"df00010000"), + + // fixext 1 + (List(MsgpackItem.Extension(0x54.toByte, hex"ab")), hex"d454ab", hex"c90000000154ab"), + // fixext 2 + (List(MsgpackItem.Extension(0x54.toByte, hex"abcd")), hex"d554abcd", hex"c90000000254abcd"), + // fixext 4 + (List(MsgpackItem.Extension(0x54.toByte, hex"abcdef01")), hex"d654abcdef01", hex"c90000000454abcdef01"), + // fixext 8 + (List(MsgpackItem.Extension(0x54.toByte, hex"abcdef0123456789")), + hex"d754abcdef0123456789", + hex"c90000000854abcdef0123456789"), + // fixext 8 + (List(MsgpackItem.Extension(0x54.toByte, hex"abcdef0123456789abcdef0123456789")), + hex"d854abcdef0123456789abcdef0123456789", + hex"c90000001054abcdef0123456789abcdef0123456789"), + + // ext 8 + (List(MsgpackItem.Extension(0x54, hex"ab".padLeft(17))), + hex"c71154" ++ hex"ab".padLeft(17), + hex"c90000001154" ++ hex"ab".padLeft(17)), + + // ext 16 + (List(MsgpackItem.Extension(0x54, hex"ab".padLeft(Math.pow(2, 8).toLong))), + hex"c8010054" ++ hex"ab".padLeft(Math.pow(2, 8).toLong), + hex"c90000010054" ++ hex"ab".padLeft(Math.pow(2, 8).toLong)), + + // ext 32 + (List(MsgpackItem.Extension(0x54, hex"ab".padLeft(Math.pow(2, 16).toLong))), + hex"c90001000054" ++ hex"ab".padLeft(Math.pow(2, 16).toLong), + hex"c90001000054" ++ hex"ab".padLeft(Math.pow(2, 16).toLong)), + + // timestamp 32 + (List(MsgpackItem.Timestamp32(0x0123abcd)), hex"d6ff0123abcd", hex"d6ff0123abcd"), + + // timestamp 64 + (List(MsgpackItem.Timestamp64(0x0123456789abcdefL)), hex"d7ff0123456789abcdef", hex"d7ff0123456789abcdef"), + + // timestamp 96 + (List(MsgpackItem.Timestamp96(0x0123abcd, 0x0123456789abcdefL)), + hex"c70cff0123abcd0123456789abcdef", + hex"c70cff0123abcd0123456789abcdef") ) Stream @@ -63,6 +157,10 @@ object SerializerSpec extends SimpleIOSuite { .through(low.bytes(true, false)) .compile .fold(ByteVector.empty)(_ :+ _) + .flatTap { got => + if (got != compressed) IO.println((got(0), compressed(0))) + else IO.unit + } .map(expect.same(_, compressed)) e2 <- From 9bc1452a236663e685af99e98a64f654c687e9ed Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Tue, 13 Aug 2024 21:18:38 +0200 Subject: [PATCH 04/33] Remove debug code in serializer test --- msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala | 4 ---- 1 file changed, 4 deletions(-) diff --git a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala index ba89f681..2a9a6154 100644 --- a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala +++ b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala @@ -157,10 +157,6 @@ object SerializerSpec extends SimpleIOSuite { .through(low.bytes(true, false)) .compile .fold(ByteVector.empty)(_ :+ _) - .flatTap { got => - if (got != compressed) IO.println((got(0), compressed(0))) - else IO.unit - } .map(expect.same(_, compressed)) e2 <- From eed74a4b04bedefe40c3bc73a7fbca8f2be829cf Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Thu, 15 Aug 2024 21:59:37 +0200 Subject: [PATCH 05/33] Add validation test cases --- .../fs2/data/msgpack/SerializerSpec.scala | 53 ++------- .../fs2/data/msgpack/ValidationSpec.scala | 103 ++++++++++++++++++ 2 files changed, 112 insertions(+), 44 deletions(-) create mode 100644 msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala diff --git a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala index 2a9a6154..90351d23 100644 --- a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala +++ b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala @@ -18,14 +18,12 @@ package fs2 package data package msgpack -import weaver.SimpleIOSuite -import scodec.bits.* -import low.MsgpackItem -import cats.effect.* -import low.internal.ValidationErrorAt -import low.internal.ValidationError +import cats.effect._ +import scodec.bits._ +import weaver._ import java.nio.charset.StandardCharsets +import low.MsgpackItem object SerializerSpec extends SimpleIOSuite { test("MessagePack item serializer should correctly serialize all formats") { @@ -154,7 +152,7 @@ object SerializerSpec extends SimpleIOSuite { e1 <- Stream .emits(source) - .through(low.bytes(true, false)) + .through(low.bytes[IO](true, false)) .compile .fold(ByteVector.empty)(_ :+ _) .map(expect.same(_, compressed)) @@ -162,7 +160,7 @@ object SerializerSpec extends SimpleIOSuite { e2 <- Stream .emits(source) - .through(low.bytes(false, false)) + .through(low.bytes[IO](false, false)) .compile .fold(ByteVector.empty)(_ :+ _) .map(expect.same(_, fast)) @@ -171,7 +169,6 @@ object SerializerSpec extends SimpleIOSuite { .compile .foldMonoid } - test("MessagePack item serializer should be fix point when optimizing for size") { val cases = List( hex"CB3FCB5A858793DD98", @@ -184,8 +181,8 @@ object SerializerSpec extends SimpleIOSuite { .evalMap { hex => Stream .chunk(Chunk.byteVector(hex)) - .through(low.items) - .through(low.toBinary) + .through(low.items[IO]) + .through(low.toBinary[IO]) .compile .toList .map(x => expect.same(ByteVector(x), hex)) @@ -194,36 +191,4 @@ object SerializerSpec extends SimpleIOSuite { .foldMonoid } - test("MessagePack item validator should raise for all checks") { - val cases = List( - List(MsgpackItem.UnsignedInt(hex"10000000000000000")) -> new ValidationErrorAt(0, "Unsigned int exceeds 64 bits"), - List(MsgpackItem.SignedInt(hex"10000000000000000")) -> new ValidationErrorAt(0, "Signed int exceeds 64 bits"), - - // TODO: Float32, Float64 - - List(MsgpackItem.Str(ByteVector.fill(Math.pow(2, 32).toLong)(1))) -> new ValidationErrorAt( - 0, - "String exceeds (2^32)-1 bytes"), - List(MsgpackItem.Bin(ByteVector.fill(Math.pow(2, 32).toLong)(1))) -> new ValidationErrorAt( - 0, - "Bin exceeds (2^32)-1 bytes"), - List(MsgpackItem.Array(2), MsgpackItem.True) -> new ValidationError("Unexpected end of input (starting at 0)"), - List(MsgpackItem.Map(1), MsgpackItem.Array(1), MsgpackItem.True) -> new ValidationError( - "Unexpected end of input (starting at 0)") - ) - - Stream - .emits(cases) - .evalMap { case (lhs, rhs) => - Stream - .emits(lhs) - .through(low.validated[IO]) - .compile - .toList - .map(x => failure(s"Expected error for item ${x}")) - .handleErrorWith(err => IO(expect.same(err, rhs))) - } - .compile - .foldMonoid - } -} +} \ No newline at end of file diff --git a/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala new file mode 100644 index 00000000..4431d288 --- /dev/null +++ b/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala @@ -0,0 +1,103 @@ +/* + * Copyright 2024 fs2-data Project + * + * 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 fs2 +package data +package msgpack + +import cats.effect._ +import low.MsgpackItem +import fs2.data.msgpack.low.internal.{ValidationError, ValidationErrorAt} +import scodec.bits.ByteVector +import weaver._ +import scodec.bits._ + +import cats.implicits._ + +object ValidationSpec extends SimpleIOSuite { + def validation1[F[_]: Sync](cases: (MsgpackItem, Throwable)*): F[Expectations] = + Stream + .emits(cases) + .evalMap { case (lhs, rhs) => + Stream + .emit(lhs) + .through(low.toBinary[F]) + .compile + .drain + .map(_ => failure(s"Expected error for item ${lhs}")) + .handleError(expect.same(_, rhs)) + } + .compile + .foldMonoid + + def validation[F[_]: Sync](cases: (List[MsgpackItem], Throwable)*): F[Expectations] = + Stream + .emits(cases) + .evalMap { case (lhs, rhs) => + Stream + .emits(lhs) + .through(low.toBinary[F]) + .compile + .drain + .map(_ => failure(s"Expected error for item ${lhs}")) + .handleError(expect.same(_, rhs)) + } + .compile + .foldMonoid + + + test("should raise if integer values exceed 64 bits") { + validation1( + MsgpackItem.UnsignedInt(hex"10000000000000000") -> new ValidationErrorAt(0, "Unsigned int exceeds 64 bits"), + MsgpackItem.SignedInt(hex"10000000000000000")-> new ValidationErrorAt(0, "Signed int exceeds 64 bits") + ) + } + + test("should raise if string or binary values exceed 2^32 - 1 bytes") { + validation1( + MsgpackItem.Str(ByteVector.empty.padLeft(Math.pow(2, 32).toLong)) -> new ValidationErrorAt(0, "String exceeds (2^32)-1 bytes"), + MsgpackItem.Bin(ByteVector.empty.padLeft(Math.pow(2, 32).toLong)) -> new ValidationErrorAt(0, "Bin exceeds (2^32)-1 bytes"), + ) + } + + test("should raise on unexpected end of input") { + validation( + List(MsgpackItem.Array(2), MsgpackItem.True) -> new ValidationError("Unexpected end of input (starting at 0)"), + List(MsgpackItem.Array(2), MsgpackItem.Array(1), MsgpackItem.True) -> new ValidationError("Unexpected end of input (starting at 0)"), + List(MsgpackItem.Array(1), MsgpackItem.Array(1)) -> new ValidationError("Unexpected end of input (starting at 1)"), + List(MsgpackItem.Array(0), MsgpackItem.Array(1)) -> new ValidationError("Unexpected end of input (starting at 1)"), + + List(MsgpackItem.Map(1), MsgpackItem.True) -> new ValidationError("Unexpected end of input (starting at 0)"), + List(MsgpackItem.Map(1), MsgpackItem.Map(1), MsgpackItem.True, MsgpackItem.True) -> new ValidationError("Unexpected end of input (starting at 0)"), + List(MsgpackItem.Map(2), MsgpackItem.True, MsgpackItem.Map(1)) -> new ValidationError("Unexpected end of input (starting at 2)"), + List(MsgpackItem.Map(2), MsgpackItem.True, MsgpackItem.Map(1)) -> new ValidationError("Unexpected end of input (starting at 2)"), + List(MsgpackItem.Map(0), MsgpackItem.Map(1)) -> new ValidationError("Unexpected end of input (starting at 1)"), + ) + } + + test("validator should raise if extension data exceeds 2^32 - 1 bytes") { + validation1( + MsgpackItem.Extension(0x54, ByteVector.empty.padLeft(Math.pow(2, 32).toLong)) -> new ValidationErrorAt(0, "Extension data exceeds (2^32)-1 bytes"), + ) + } + + test("should raise if nanoseconds fields exceed 999999999") { + validation1( + MsgpackItem.Timestamp64(0xEE6B280000000000L)-> new ValidationErrorAt(0, "Timestamp64 nanoseconds cannot be larger than '999999999'"), + MsgpackItem.Timestamp96(1000000000, 0)-> new ValidationErrorAt(0, "Timestamp96 nanoseconds cannot be larger than '999999999'"), + ) + } +} From 54d7d5585a79ba0ba192ef1127fee8366cd33ff0 Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Sat, 17 Aug 2024 13:02:59 +0200 Subject: [PATCH 06/33] Make msgpack item serializer omit leading zeros This change applies only to types in which leading zeros are insignificant. --- .../msgpack/low/internal/ItemSerializer.scala | 71 ++++++++++--------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala index cd6a2473..9495b7e8 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala @@ -25,28 +25,30 @@ import scodec.bits._ private[low] object ItemSerializer { def compressed: MsgpackItem => ByteVector = { case MsgpackItem.UnsignedInt(bytes) => - if (bytes.size <= 1) - ByteVector(Headers.Uint8).buffer ++ bytes.padLeft(1) - else if (bytes.size <= 2) - ByteVector(Headers.Uint16).buffer ++ bytes.padLeft(2) - else if (bytes.size <= 4) - ByteVector(Headers.Uint32).buffer ++ bytes.padLeft(4) + val bs = bytes.dropWhile(_ == 0) + if (bs.size <= 1) + ByteVector(Headers.Uint8).buffer ++ bs.padLeft(1) + else if (bs.size <= 2) + ByteVector(Headers.Uint16).buffer ++ bs.padLeft(2) + else if (bs.size <= 4) + ByteVector(Headers.Uint32).buffer ++ bs.padLeft(4) else - ByteVector(Headers.Uint64).buffer ++ bytes.padLeft(8) + ByteVector(Headers.Uint64).buffer ++ bs.padLeft(8) case MsgpackItem.SignedInt(bytes) => - if (bytes.size <= 1) + val bs = bytes.dropWhile(_ == 0) + if (bs.size <= 1) // positive fixint or negative fixint - if ((bytes & hex"7f") == bytes || (bytes & hex"c0") == hex"c0") - bytes.padLeft(1) + if ((bs & hex"7f") == bs || (bs & hex"c0") == hex"c0") + bs.padLeft(1) else - ByteVector(Headers.Int8).buffer ++ bytes.padLeft(1) - else if (bytes.size <= 2) - ByteVector(Headers.Int16).buffer ++ bytes.padLeft(2) - else if (bytes.size <= 4) - ByteVector(Headers.Int32).buffer ++ bytes.padLeft(4) + ByteVector(Headers.Int8).buffer ++ bs.padLeft(1) + else if (bs.size <= 2) + ByteVector(Headers.Int16).buffer ++ bs.padLeft(2) + else if (bs.size <= 4) + ByteVector(Headers.Int32).buffer ++ bs.padLeft(4) else - ByteVector(Headers.Int64).buffer ++ bytes.padLeft(8) + ByteVector(Headers.Int64).buffer ++ bs.padLeft(8) case MsgpackItem.Float32(float) => ByteVector(Headers.Float32).buffer ++ ByteVector.fromInt(java.lang.Float.floatToIntBits(float)) @@ -103,25 +105,26 @@ private[low] object ItemSerializer { } case MsgpackItem.Extension(tpe, bytes) => - if (bytes.size <= 1) { - (ByteVector(Headers.FixExt1).buffer :+ tpe) ++ bytes.padLeft(1) - } else if (bytes.size <= 2) { - (ByteVector(Headers.FixExt2).buffer :+ tpe) ++ bytes.padLeft(2) - } else if (bytes.size <= 4) { - (ByteVector(Headers.FixExt4).buffer :+ tpe) ++ bytes.padLeft(4) - } else if (bytes.size <= 8) { - (ByteVector(Headers.FixExt8).buffer :+ tpe) ++ bytes.padLeft(8) - } else if (bytes.size <= 16) { - (ByteVector(Headers.FixExt16).buffer :+ tpe) ++ bytes.padLeft(16) - } else if (bytes.size <= Math.pow(2, 8) - 1) { - val size = ByteVector.fromByte(bytes.size.toByte) - (ByteVector(Headers.Ext8).buffer ++ size :+ tpe) ++ bytes - } else if (bytes.size <= Math.pow(2, 16) - 1) { - val size = ByteVector.fromShort(bytes.size.toShort) - (ByteVector(Headers.Ext16).buffer ++ size :+ tpe) ++ bytes + val bs = bytes.dropWhile(_ == 0) + if (bs.size <= 1) { + (ByteVector(Headers.FixExt1).buffer :+ tpe) ++ bs.padLeft(1) + } else if (bs.size <= 2) { + (ByteVector(Headers.FixExt2).buffer :+ tpe) ++ bs.padLeft(2) + } else if (bs.size <= 4) { + (ByteVector(Headers.FixExt4).buffer :+ tpe) ++ bs.padLeft(4) + } else if (bs.size <= 8) { + (ByteVector(Headers.FixExt8).buffer :+ tpe) ++ bs.padLeft(8) + } else if (bs.size <= 16) { + (ByteVector(Headers.FixExt16).buffer :+ tpe) ++ bs.padLeft(16) + } else if (bs.size <= Math.pow(2, 8) - 1) { + val size = ByteVector.fromByte(bs.size.toByte) + (ByteVector(Headers.Ext8).buffer ++ size :+ tpe) ++ bs + } else if (bs.size <= Math.pow(2, 16) - 1) { + val size = ByteVector.fromShort(bs.size.toShort) + (ByteVector(Headers.Ext16).buffer ++ size :+ tpe) ++ bs } else { - val size = ByteVector.fromInt(bytes.size.toInt) - (ByteVector(Headers.Ext32).buffer ++ size :+ tpe) ++ bytes + val size = ByteVector.fromInt(bs.size.toInt) + (ByteVector(Headers.Ext32).buffer ++ size :+ tpe) ++ bs } case MsgpackItem.Timestamp32(seconds) => From 1ed1f73169d1d47d1b4ecd9f5b1042646e91d820 Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Sun, 18 Aug 2024 18:06:28 +0200 Subject: [PATCH 07/33] Make Extension tests use `ByteVector.fill` --- .../fs2/data/msgpack/SerializerSpec.scala | 18 +++++++++--------- .../fs2/data/msgpack/ValidationSpec.scala | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala index 90351d23..c2e28279 100644 --- a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala +++ b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala @@ -119,19 +119,19 @@ object SerializerSpec extends SimpleIOSuite { hex"c90000001054abcdef0123456789abcdef0123456789"), // ext 8 - (List(MsgpackItem.Extension(0x54, hex"ab".padLeft(17))), - hex"c71154" ++ hex"ab".padLeft(17), - hex"c90000001154" ++ hex"ab".padLeft(17)), + (List(MsgpackItem.Extension(0x54, ByteVector.fill(17)(0xab))), + hex"c71154" ++ ByteVector.fill(17)(0xab), + hex"c90000001154" ++ ByteVector.fill(17)(0xab)), // ext 16 - (List(MsgpackItem.Extension(0x54, hex"ab".padLeft(Math.pow(2, 8).toLong))), - hex"c8010054" ++ hex"ab".padLeft(Math.pow(2, 8).toLong), - hex"c90000010054" ++ hex"ab".padLeft(Math.pow(2, 8).toLong)), + (List(MsgpackItem.Extension(0x54, ByteVector.fill(Math.pow(2, 8).toLong)(0xab))), + hex"c8010054" ++ ByteVector.fill(Math.pow(2, 8).toLong)(0xab), + hex"c90000010054" ++ ByteVector.fill(Math.pow(2, 8).toLong)(0xab)), // ext 32 - (List(MsgpackItem.Extension(0x54, hex"ab".padLeft(Math.pow(2, 16).toLong))), - hex"c90001000054" ++ hex"ab".padLeft(Math.pow(2, 16).toLong), - hex"c90001000054" ++ hex"ab".padLeft(Math.pow(2, 16).toLong)), + (List(MsgpackItem.Extension(0x54, ByteVector.fill(Math.pow(2, 16).toLong)(0xab))), + hex"c90001000054" ++ ByteVector.fill(Math.pow(2, 16).toLong)(0xab), + hex"c90001000054" ++ ByteVector.fill(Math.pow(2, 16).toLong)(0xab)), // timestamp 32 (List(MsgpackItem.Timestamp32(0x0123abcd)), hex"d6ff0123abcd", hex"d6ff0123abcd"), diff --git a/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala index 4431d288..a777ffd5 100644 --- a/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala +++ b/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala @@ -88,7 +88,7 @@ object ValidationSpec extends SimpleIOSuite { ) } - test("validator should raise if extension data exceeds 2^32 - 1 bytes") { + test("should raise if extension data exceeds 2^32 - 1 bytes") { validation1( MsgpackItem.Extension(0x54, ByteVector.empty.padLeft(Math.pow(2, 32).toLong)) -> new ValidationErrorAt(0, "Extension data exceeds (2^32)-1 bytes"), ) From ac1452848d5f72ff5af755acda07655edb9baf9f Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Sun, 18 Aug 2024 18:07:17 +0200 Subject: [PATCH 08/33] Refine msgpack fixpoint test The parser mapping ByteVector to MsgpackItem can be seen as a not injective morphism, that is, there are many ByteVectors that will map to the same MsgpackItem. Because of this, we cannot possibly guarantee that `serialize(parse(bs))` is fixpoint for an arbitrary `bs`. However, currently implemented serializers *are* injective (if we exclude the Timestamp format family as it can be represented with Extension types) and so, we can guarantee `serialize(parse(bs)) == bs` if `bs` is a member of a subset of ByteVector that is emitted by a serializer. In other words, the following code will be true for any `bs` if `serialize` is injective and we ignore the Timestamp type family: ``` val first = serialize(parse(bs)) val second = serialize(parse(first)) first == second ``` This test makes sure that the above holds. --- .../fs2/data/msgpack/SerializerSpec.scala | 59 +++++++++++++------ 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala index c2e28279..59f19fd9 100644 --- a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala +++ b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala @@ -169,26 +169,49 @@ object SerializerSpec extends SimpleIOSuite { .compile .foldMonoid } - test("MessagePack item serializer should be fix point when optimizing for size") { + + test("MessagePack item serializer should be fixpoint for a subset of ByteVector") { + /* The parser mapping ByteVector to MsgpackItem can be seen as a not injective morphism, that is, there + * are many ByteVectors that will map to the same MsgpackItem. Because of this, we cannot possibly guarantee that + * `serialize(parse(bs))` is fixpoint for an arbitrary `bs`. However, currently implemented serializers *are* + * injective (if we exclude the Timestamp format family as it can be represented with Extension types) and so, we + * can guarantee `serialize(parse(bs)) == bs` if `bs` is a member of a subset of ByteVector that is emitted by a + * serializer. + * + * In other words, the following code will be true for any `bs` if `serialize` is injective and we ignore the + * Timestamp type family: + * {{{ + * val first = serialize(parse(bs)) + * val second = serialize(parse(first)) + * first == second + * }}} + * + * This test makes sure that the above holds. + */ + val cases = List( - hex"CB3FCB5A858793DD98", - hex"d6ffaabbccdd", - hex"81A77765622D61707083A7736572766C65749583AC736572766C65742D6E616D65A8636F666178434453AD736572766C65742D636C617373B86F72672E636F6661782E6364732E434453536572766C6574AA696E69742D706172616DDE002ABD636F6E666967476C6F73736172793A696E7374616C6C6174696F6E4174B05068696C6164656C706869612C205041B9636F6E666967476C6F73736172793A61646D696E456D61696CAD6B736D40706F626F782E636F6DB8636F6E666967476C6F73736172793A706F77657265644279A5436F666178BC636F6E666967476C6F73736172793A706F7765726564427949636F6EB12F696D616765732F636F6661782E676966B9636F6E666967476C6F73736172793A73746174696350617468AF2F636F6E74656E742F737461746963B674656D706C61746550726F636573736F72436C617373B96F72672E636F6661782E5779736977796754656D706C617465B374656D706C6174654C6F61646572436C617373BD6F72672E636F6661782E46696C657354656D706C6174654C6F61646572AC74656D706C61746550617468A974656D706C61746573B474656D706C6174654F7665727269646550617468A0B364656661756C744C69737454656D706C617465B06C69737454656D706C6174652E68746DB364656661756C7446696C6554656D706C617465B361727469636C6554656D706C6174652E68746DA67573654A5350C2AF6A73704C69737454656D706C617465B06C69737454656D706C6174652E6A7370AF6A737046696C6554656D706C617465B361727469636C6554656D706C6174652E6A7370B563616368655061636B61676554616773547261636BCCC8B563616368655061636B6167655461677353746F7265CCC8B763616368655061636B61676554616773526566726573683CB3636163686554656D706C61746573547261636B64B3636163686554656D706C6174657353746F726532B5636163686554656D706C61746573526566726573680FAF63616368655061676573547261636BCCC8AF6361636865506167657353746F726564B163616368655061676573526566726573680AB3636163686550616765734469727479526561640AB8736561726368456E67696E654C69737454656D706C617465B8666F72536561726368456E67696E65734C6973742E68746DB8736561726368456E67696E6546696C6554656D706C617465B4666F72536561726368456E67696E65732E68746DB4736561726368456E67696E65526F626F74734462B15745422D494E462F726F626F74732E6462AC7573654461746153746F7265C3AE6461746153746F7265436C617373B66F72672E636F6661782E53716C4461746153746F7265B07265646972656374696F6E436C617373B86F72672E636F6661782E53716C5265646972656374696F6EAD6461746153746F72654E616D65A5636F666178AF6461746153746F7265447269766572D92C636F6D2E6D6963726F736F66742E6A6462632E73716C7365727665722E53514C536572766572447269766572AC6461746153746F726555726CD93B6A6462633A6D6963726F736F66743A73716C7365727665723A2F2F4C4F43414C484F53543A313433333B44617461626173654E616D653D676F6F6EAD6461746153746F726555736572A27361B16461746153746F726550617373776F7264B26461746153746F7265546573745175657279B26461746153746F7265546573745175657279D922534554204E4F434F554E54204F4E3B73656C65637420746573743D2774657374273BB06461746153746F72654C6F6746696C65D9242F7573722F6C6F63616C2F746F6D6361742F6C6F67732F6461746173746F72652E6C6F67B26461746153746F7265496E6974436F6E6E730AB16461746153746F72654D6178436F6E6E7364B76461746153746F7265436F6E6E55736167654C696D697464B16461746153746F72654C6F674C6576656CA56465627567AC6D617855726C4C656E677468CD01F483AC736572766C65742D6E616D65AA636F666178456D61696CAD736572766C65742D636C617373BA6F72672E636F6661782E6364732E456D61696C536572766C6574AA696E69742D706172616D82A86D61696C486F7374A56D61696C31B06D61696C486F73744F76657272696465A56D61696C3282AC736572766C65742D6E616D65AA636F66617841646D696EAD736572766C65742D636C617373BA6F72672E636F6661782E6364732E41646D696E536572766C657482AC736572766C65742D6E616D65AB66696C65536572766C6574AD736572766C65742D636C617373B96F72672E636F6661782E6364732E46696C65536572766C657483AC736572766C65742D6E616D65AA636F666178546F6F6C73AD736572766C65742D636C617373BF6F72672E636F6661782E636D732E436F666178546F6F6C73536572766C6574AA696E69742D706172616D8DAC74656D706C61746550617468AF746F6F6C7374656D706C617465732FA36C6F6701AB6C6F674C6F636174696F6ED9252F7573722F6C6F63616C2F746F6D6361742F6C6F67732F436F666178546F6F6C732E6C6F67AA6C6F674D617853697A65A0A7646174614C6F6701AF646174614C6F674C6F636174696F6ED9222F7573722F6C6F63616C2F746F6D6361742F6C6F67732F646174614C6F672E6C6F67AE646174614C6F674D617853697A65A0AF72656D6F7665506167654361636865D9252F636F6E74656E742F61646D696E2F72656D6F76653F63616368653D70616765732669643DB372656D6F766554656D706C6174654361636865D9292F636F6E74656E742F61646D696E2F72656D6F76653F63616368653D74656D706C617465732669643DB266696C655472616E73666572466F6C646572D9342F7573722F6C6F63616C2F746F6D6361742F776562617070732F636F6E74656E742F66696C655472616E73666572466F6C646572AD6C6F6F6B496E436F6E7465787401AC61646D696E47726F7570494404AA62657461536572766572C3AF736572766C65742D6D617070696E6785A8636F666178434453A12FAA636F666178456D61696CB32F636F6661787574696C2F61656D61696C2F2AAA636F66617841646D696EA82F61646D696E2F2AAB66696C65536572766C6574A92F7374617469632F2AAA636F666178546F6F6C73A82F746F6F6C732F2AA67461676C696282AA7461676C69622D757269A9636F6661782E746C64AF7461676C69622D6C6F636174696F6EB72F5745422D494E462F746C64732F636F6661782E746C64" + hex"918FA46461746582A662756666657282A474797065A6427566666572A4646174619401234567A474797065CCFFA35F6964B8363663316233363661333137353434376163346335343165A5696E64657800A467756964D92438666665653537302D353938312D346630362D623635382D653435383163363064373539A86973416374697665C3A762616C616E6365CB40A946956A97C84CA361676516A8657965436F6C6F72A4626C7565A46E616D65AD4D6F72746F6E204C6974746C65A761646472657373D9313933372044656172626F726E20436F7572742C204861726C656967682C204D6173736163687573657474732C2033353936AA72656769737465726564BA323032332D30382D32395431303A34353A3335202D30323A3030A86C61746974756465CB4047551159C49774A96C6F6E676974756465CBC065F94A771C970FA47461677397A54C6F72656DA3657374A86465736572756E74A54C6F72656DA46E697369A76C61626F726973A86465736572756E74A7667269656E64739382A2696400A46E616D65B04865726E616E64657A204C6172736F6E82A2696401A46E616D65AF4D616E6E696E672053617267656E7482A2696402A46E616D65AF536176616E6E6168204E65776D616E" ) - Stream - .emits(cases) - .evalMap { hex => - Stream - .chunk(Chunk.byteVector(hex)) - .through(low.items[IO]) - .through(low.toBinary[IO]) - .compile - .toList - .map(x => expect.same(ByteVector(x), hex)) + def round(data: ByteVector, compress: Boolean) = + Stream + .chunk(Chunk.byteVector(data)) + .through(low.items[IO]) + .through(low.bytes[IO](compress, false)) + .fold(ByteVector.empty)(_ :+ _) + + def process(compress: Boolean, serializerName: String) = + for { + data <- Stream.emits(cases) + pre <- round(data, compress) + processed <- round(pre, compress) + } yield { + if (processed == pre) + success + else + failure(s"${serializerName} should be fixpoint for: ${pre} but it emitted ${processed}") } - .compile - .foldMonoid - } -} \ No newline at end of file + (process(true, "ItemSerializer.compressed") ++ process(false, "ItemSerializer.none")).compile.foldMonoid + } +} From 671e693598c9dd142ce07f5eddf70fe0e0d3c010 Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Sun, 18 Aug 2024 21:13:38 +0200 Subject: [PATCH 09/33] Remove redundant `padLeft`s when size is known --- .../fs2/data/msgpack/low/internal/ItemSerializer.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala index 9495b7e8..9f49ad44 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala @@ -78,7 +78,7 @@ private[low] object ItemSerializer { val size = ByteVector.fromShort(bytes.size.toShort) ByteVector(Headers.Bin16).buffer ++ size ++ bytes } else { - val size = ByteVector.fromInt(bytes.size.toInt).padLeft(4) + val size = ByteVector.fromInt(bytes.size.toInt) ByteVector(Headers.Bin32).buffer ++ size ++ bytes } @@ -170,10 +170,10 @@ private[low] object ItemSerializer { ByteVector(Headers.Bin32) ++ size ++ item.bytes case item: MsgpackItem.Array => - ByteVector(Headers.Array32) ++ ByteVector.fromInt(item.size).padLeft(4) + ByteVector(Headers.Array32) ++ ByteVector.fromInt(item.size) case item: MsgpackItem.Map => - ByteVector(Headers.Map32) ++ ByteVector.fromInt(item.size).padLeft(4) + ByteVector(Headers.Map32) ++ ByteVector.fromInt(item.size) case item: MsgpackItem.Extension => val size = ByteVector.fromInt(item.bytes.size.toInt) From b73258346c80bbfe178cf6a0838fd4e5e9ce22fe Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Sun, 18 Aug 2024 22:00:54 +0200 Subject: [PATCH 10/33] Reformat ValidationSpec.scala --- .../fs2/data/msgpack/ValidationSpec.scala | 44 ++++++++++++------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala index a777ffd5..9ef00a2e 100644 --- a/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala +++ b/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala @@ -58,46 +58,60 @@ object ValidationSpec extends SimpleIOSuite { .compile .foldMonoid - test("should raise if integer values exceed 64 bits") { validation1( MsgpackItem.UnsignedInt(hex"10000000000000000") -> new ValidationErrorAt(0, "Unsigned int exceeds 64 bits"), - MsgpackItem.SignedInt(hex"10000000000000000")-> new ValidationErrorAt(0, "Signed int exceeds 64 bits") + MsgpackItem.SignedInt(hex"10000000000000000") -> new ValidationErrorAt(0, "Signed int exceeds 64 bits") ) } test("should raise if string or binary values exceed 2^32 - 1 bytes") { validation1( - MsgpackItem.Str(ByteVector.empty.padLeft(Math.pow(2, 32).toLong)) -> new ValidationErrorAt(0, "String exceeds (2^32)-1 bytes"), - MsgpackItem.Bin(ByteVector.empty.padLeft(Math.pow(2, 32).toLong)) -> new ValidationErrorAt(0, "Bin exceeds (2^32)-1 bytes"), + MsgpackItem.Str(ByteVector.empty.padLeft(Math.pow(2, 32).toLong)) -> new ValidationErrorAt( + 0, + "String exceeds (2^32)-1 bytes"), + MsgpackItem.Bin(ByteVector.empty.padLeft(Math.pow(2, 32).toLong)) -> new ValidationErrorAt( + 0, + "Bin exceeds (2^32)-1 bytes") ) } test("should raise on unexpected end of input") { validation( List(MsgpackItem.Array(2), MsgpackItem.True) -> new ValidationError("Unexpected end of input (starting at 0)"), - List(MsgpackItem.Array(2), MsgpackItem.Array(1), MsgpackItem.True) -> new ValidationError("Unexpected end of input (starting at 0)"), - List(MsgpackItem.Array(1), MsgpackItem.Array(1)) -> new ValidationError("Unexpected end of input (starting at 1)"), - List(MsgpackItem.Array(0), MsgpackItem.Array(1)) -> new ValidationError("Unexpected end of input (starting at 1)"), - + List(MsgpackItem.Array(2), MsgpackItem.Array(1), MsgpackItem.True) -> new ValidationError( + "Unexpected end of input (starting at 0)"), + List(MsgpackItem.Array(1), MsgpackItem.Array(1)) -> new ValidationError( + "Unexpected end of input (starting at 1)"), + List(MsgpackItem.Array(0), MsgpackItem.Array(1)) -> new ValidationError( + "Unexpected end of input (starting at 1)"), List(MsgpackItem.Map(1), MsgpackItem.True) -> new ValidationError("Unexpected end of input (starting at 0)"), - List(MsgpackItem.Map(1), MsgpackItem.Map(1), MsgpackItem.True, MsgpackItem.True) -> new ValidationError("Unexpected end of input (starting at 0)"), - List(MsgpackItem.Map(2), MsgpackItem.True, MsgpackItem.Map(1)) -> new ValidationError("Unexpected end of input (starting at 2)"), - List(MsgpackItem.Map(2), MsgpackItem.True, MsgpackItem.Map(1)) -> new ValidationError("Unexpected end of input (starting at 2)"), - List(MsgpackItem.Map(0), MsgpackItem.Map(1)) -> new ValidationError("Unexpected end of input (starting at 1)"), + List(MsgpackItem.Map(1), MsgpackItem.Map(1), MsgpackItem.True, MsgpackItem.True) -> new ValidationError( + "Unexpected end of input (starting at 0)"), + List(MsgpackItem.Map(2), MsgpackItem.True, MsgpackItem.Map(1)) -> new ValidationError( + "Unexpected end of input (starting at 2)"), + List(MsgpackItem.Map(2), MsgpackItem.True, MsgpackItem.Map(1)) -> new ValidationError( + "Unexpected end of input (starting at 2)"), + List(MsgpackItem.Map(0), MsgpackItem.Map(1)) -> new ValidationError("Unexpected end of input (starting at 1)") ) } test("should raise if extension data exceeds 2^32 - 1 bytes") { validation1( - MsgpackItem.Extension(0x54, ByteVector.empty.padLeft(Math.pow(2, 32).toLong)) -> new ValidationErrorAt(0, "Extension data exceeds (2^32)-1 bytes"), + MsgpackItem.Extension(0x54, ByteVector.empty.padLeft(Math.pow(2, 32).toLong)) -> new ValidationErrorAt( + 0, + "Extension data exceeds (2^32)-1 bytes") ) } test("should raise if nanoseconds fields exceed 999999999") { validation1( - MsgpackItem.Timestamp64(0xEE6B280000000000L)-> new ValidationErrorAt(0, "Timestamp64 nanoseconds cannot be larger than '999999999'"), - MsgpackItem.Timestamp96(1000000000, 0)-> new ValidationErrorAt(0, "Timestamp96 nanoseconds cannot be larger than '999999999'"), + MsgpackItem.Timestamp64(0xee6b280000000000L) -> new ValidationErrorAt( + 0, + "Timestamp64 nanoseconds cannot be larger than '999999999'"), + MsgpackItem.Timestamp96(1000000000, 0) -> new ValidationErrorAt( + 0, + "Timestamp96 nanoseconds cannot be larger than '999999999'") ) } } From 3f30e33ef59a5a015726750c0d71e31169730625 Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Sun, 18 Aug 2024 22:24:07 +0200 Subject: [PATCH 11/33] Remove scaladoc from an embedded function --- .../scala/fs2/data/msgpack/low/internal/ItemValidator.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala index d9402fee..20cdfb49 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala @@ -34,8 +34,6 @@ private[low] object ItemValidator { def none[F[_]]: Pipe[F, MsgpackItem, MsgpackItem] = in => in def simple[F[_]](implicit F: RaiseThrowable[F]): Pipe[F, MsgpackItem, MsgpackItem] = { in => - /** Validates one item from a stream - */ def step1(chunk: Chunk[MsgpackItem], idx: Int, position: Long): Pull[F, MsgpackItem, Option[Expect]] = chunk(idx) match { case MsgpackItem.UnsignedInt(bytes) => From aa8658ae5f06cd6ce825811e08c2cc447d99fd5a Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Sat, 24 Aug 2024 15:39:23 +0200 Subject: [PATCH 12/33] Add benchmars for msgpack item serializer --- .../MsgPackItemSerializerBenchmarks.scala | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemSerializerBenchmarks.scala diff --git a/benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemSerializerBenchmarks.scala b/benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemSerializerBenchmarks.scala new file mode 100644 index 00000000..93801e9f --- /dev/null +++ b/benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemSerializerBenchmarks.scala @@ -0,0 +1,71 @@ +/* + * Copyright 2024 fs2-data Project + * + * 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 fs2 +package data.benchmarks + +import java.util.concurrent.TimeUnit +import org.openjdk.jmh.annotations._ + +import cats.effect.SyncIO + +import scodec.bits._ +import fs2._ + +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@BenchmarkMode(Array(Mode.AverageTime)) +@State(org.openjdk.jmh.annotations.Scope.Benchmark) +@Fork(value = 1) +@Warmup(iterations = 3, time = 2) +@Measurement(iterations = 10, time = 2) +class MsgPackItemSerializerBenchmarks { + val msgpackItems: List[fs2.data.msgpack.low.MsgpackItem] = { + val bytes = + fs2.io + .readClassLoaderResource[SyncIO]("twitter_msgpack.txt", 4096) + .through(fs2.text.utf8.decode) + .compile + .string + .map(ByteVector.fromHex(_).get) + .unsafeRunSync() + + Stream + .chunk(Chunk.byteVector(bytes)) + .through(fs2.data.msgpack.low.items[SyncIO]) + .compile + .toList + .unsafeRunSync() + } + + + @Benchmark + def compressed() = + Stream + .emits(msgpackItems) + .through(fs2.data.msgpack.low.bytes[SyncIO](true, false)) + .compile + .drain + .unsafeRunSync() + + @Benchmark + def fast() = + Stream + .emits(msgpackItems) + .through(fs2.data.msgpack.low.bytes[SyncIO](false, false)) + .compile + .drain + .unsafeRunSync() +} From cd9782e0918d73677d76afefcd2b01af053f0b9f Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Thu, 5 Sep 2024 21:38:39 +0200 Subject: [PATCH 13/33] Merge msgpack serializers - There was very little performance difference between serializers so the `fast` serializer was entirely scrapped. - The current serializer buffers the output in 4KiB segments before emitting it. This change brought a significant speedup. --- .../MsgPackItemSerializerBenchmarks.scala | 8 +- .../msgpack/low/internal/ItemSerializer.scala | 226 +++++++++++------- .../msgpack/low/internal/ItemValidator.scala | 4 +- .../scala/fs2/data/msgpack/low/package.scala | 31 +-- .../fs2/data/msgpack/SerializerSpec.scala | 147 +++++------- 5 files changed, 207 insertions(+), 209 deletions(-) diff --git a/benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemSerializerBenchmarks.scala b/benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemSerializerBenchmarks.scala index 93801e9f..325bb16d 100644 --- a/benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemSerializerBenchmarks.scala +++ b/benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemSerializerBenchmarks.scala @@ -52,19 +52,19 @@ class MsgPackItemSerializerBenchmarks { @Benchmark - def compressed() = + def serialize() = Stream .emits(msgpackItems) - .through(fs2.data.msgpack.low.bytes[SyncIO](true, false)) + .through(fs2.data.msgpack.low.bytes[SyncIO](false)) .compile .drain .unsafeRunSync() @Benchmark - def fast() = + def withValidation() = Stream .emits(msgpackItems) - .through(fs2.data.msgpack.low.bytes[SyncIO](false, false)) + .through(fs2.data.msgpack.low.bytes[SyncIO](true)) .compile .drain .unsafeRunSync() diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala index 9f49ad44..09bbdf6d 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala @@ -23,181 +23,225 @@ package internal import scodec.bits._ private[low] object ItemSerializer { - def compressed: MsgpackItem => ByteVector = { + class MalformedItemError extends Error("item exceeds the maximum size of it's format") + class MalformedStringError extends MalformedItemError + class MalformedBinError extends MalformedItemError + class MalformedIntError extends MalformedItemError + class MalformedUintError extends MalformedItemError + + /** Checks whether integer `x` fits in `n` bytes. */ + @inline + private def fitsIn(x: Int, n: Long): Boolean = + java.lang.Integer.compareUnsigned(x, (Math.pow(2, n.toDouble).toLong - 1).toInt) <= 0 + + private case class SerializationContext[F[_]](out: Out[F], + chunk: Chunk[MsgpackItem], + idx: Int, + rest: Stream[F, MsgpackItem]) + + /** Buffers [[Chunk]] into 4KiB segments before calling [[Pull.output]]. + * + * @param contents buffered [[Chunk]] + */ + private class Out[F[_]](contents: Chunk[Byte]) { + private val limit = 4096 + + /** Pushes `bv` into the buffer and emits the buffer if it reaches the limit. + */ + @inline + def push(bv: ByteVector): Pull[F, Byte, Out[F]] = + if (contents.size >= limit) + Pull.output(contents).as(new Out(Chunk.byteVector(bv))) + else + Pull.done.as(new Out(contents ++ Chunk.byteVector(bv))) + + /** Splices `bv` into segments and pushes them into the buffer while emitting the buffer at the same time so + * that it never exceeds the limit during the operation. + * + * Use this instead of [[Out.push]] when `bv` may significantly exceed 4KiB. + */ + def pushBuffered(bv: ByteVector): Pull[F, Byte, Out[F]] = { + @inline + def go(chunk: Chunk[Byte], rest: ByteVector): Pull[F, Byte, Out[F]] = + if (rest.isEmpty) + Pull.done.as(new Out(chunk)) + else + Pull.output(chunk) >> go(Chunk.byteVector(rest.take(limit.toLong)), rest.drop(limit.toLong)) + + if (bv.isEmpty) + this.push(bv) + else if (contents.size >= limit) + Pull.output(contents) >> go(Chunk.byteVector(bv.take(limit.toLong)), bv.drop(limit.toLong)) + else + go(contents ++ Chunk.byteVector(bv.take(limit.toLong - contents.size)), bv.drop(limit.toLong - contents.size)) + } + + /** Outputs the whole buffer. */ + @inline + def flush = Pull.output(contents) + } + + @inline + private def step[F[_]: RaiseThrowable](o: Out[F], item: MsgpackItem): Pull[F, Byte, Out[F]] = item match { case MsgpackItem.UnsignedInt(bytes) => val bs = bytes.dropWhile(_ == 0) if (bs.size <= 1) - ByteVector(Headers.Uint8).buffer ++ bs.padLeft(1) + o.push(ByteVector(Headers.Uint8) ++ bs.padLeft(1)) else if (bs.size <= 2) - ByteVector(Headers.Uint16).buffer ++ bs.padLeft(2) + o.push(ByteVector(Headers.Uint16) ++ bs.padLeft(2)) else if (bs.size <= 4) - ByteVector(Headers.Uint32).buffer ++ bs.padLeft(4) + o.push(ByteVector(Headers.Uint32) ++ bs.padLeft(4)) + else if (bs.size <= 8) + o.push(ByteVector(Headers.Uint64) ++ bs.padLeft(8)) else - ByteVector(Headers.Uint64).buffer ++ bs.padLeft(8) + Pull.raiseError(new MalformedUintError) case MsgpackItem.SignedInt(bytes) => val bs = bytes.dropWhile(_ == 0) if (bs.size <= 1) // positive fixint or negative fixint if ((bs & hex"7f") == bs || (bs & hex"c0") == hex"c0") - bs.padLeft(1) + o.push(bs.padLeft(1)) else - ByteVector(Headers.Int8).buffer ++ bs.padLeft(1) + o.push(ByteVector(Headers.Int8) ++ bs.padLeft(1)) else if (bs.size <= 2) - ByteVector(Headers.Int16).buffer ++ bs.padLeft(2) + o.push(ByteVector(Headers.Int16) ++ bs.padLeft(2)) else if (bs.size <= 4) - ByteVector(Headers.Int32).buffer ++ bs.padLeft(4) + o.push(ByteVector(Headers.Int32) ++ bs.padLeft(4)) + else if (bs.size <= 8) + o.push(ByteVector(Headers.Int64) ++ bs.padLeft(8)) else - ByteVector(Headers.Int64).buffer ++ bs.padLeft(8) + Pull.raiseError(new MalformedIntError) case MsgpackItem.Float32(float) => - ByteVector(Headers.Float32).buffer ++ ByteVector.fromInt(java.lang.Float.floatToIntBits(float)) + o.push(ByteVector(Headers.Float32) ++ ByteVector.fromInt(java.lang.Float.floatToIntBits(float))) case MsgpackItem.Float64(double) => - ByteVector(Headers.Float64).buffer ++ ByteVector.fromLong(java.lang.Double.doubleToLongBits(double)) + o.push(ByteVector(Headers.Float64) ++ ByteVector.fromLong(java.lang.Double.doubleToLongBits(double))) case MsgpackItem.Str(bytes) => if (bytes.size <= 31) { - ByteVector.fromByte((0xa0 | bytes.size).toByte).buffer ++ bytes + o.push(ByteVector.fromByte((0xa0 | bytes.size).toByte) ++ bytes) } else if (bytes.size <= Math.pow(2, 8) - 1) { val size = ByteVector.fromByte(bytes.size.toByte) - ByteVector(Headers.Str8).buffer ++ size ++ bytes + o.push(ByteVector(Headers.Str8) ++ size ++ bytes) } else if (bytes.size <= Math.pow(2, 16) - 1) { val size = ByteVector.fromShort(bytes.size.toShort) - ByteVector(Headers.Str16).buffer ++ size ++ bytes - } else { + o.push(ByteVector(Headers.Str16) ++ size ++ bytes) + } else if (fitsIn(bytes.size.toInt, 32)) { val size = ByteVector.fromInt(bytes.size.toInt) - ByteVector(Headers.Str32).buffer ++ size ++ bytes + /* Max length of str32 (incl. type and length info) is 2^32 + 4 bytes + * which is more than Chunk can handle at once + */ + o.pushBuffered(ByteVector(Headers.Str32) ++ size ++ bytes) + } else { + Pull.raiseError(new MalformedStringError) } case MsgpackItem.Bin(bytes) => if (bytes.size <= Math.pow(2, 8) - 1) { val size = ByteVector.fromByte(bytes.size.toByte) - ByteVector(Headers.Bin8).buffer ++ size ++ bytes + o.push(ByteVector(Headers.Bin8) ++ size ++ bytes) } else if (bytes.size <= Math.pow(2, 16) - 1) { val size = ByteVector.fromShort(bytes.size.toShort) - ByteVector(Headers.Bin16).buffer ++ size ++ bytes - } else { + o.push(ByteVector(Headers.Bin16) ++ size ++ bytes) + } else if (fitsIn(bytes.size.toInt, 32)) { val size = ByteVector.fromInt(bytes.size.toInt) - ByteVector(Headers.Bin32).buffer ++ size ++ bytes + /* Max length of str32 (incl. type and length info) is 2^32 + 4 bytes + * which is more than Chunk can handle at once + */ + o.pushBuffered(ByteVector(Headers.Bin32) ++ size ++ bytes) + } else { + Pull.raiseError(new MalformedBinError) } case MsgpackItem.Array(size) => - if (size <= 15) { - ByteVector.fromByte((0x90 | size).toByte) + if (fitsIn(size, 4)) { + o.push(ByteVector.fromByte((0x90 | size).toByte)) } else if (size <= Math.pow(2, 16) - 1) { val s = ByteVector.fromShort(size.toShort) - ByteVector(Headers.Array16).buffer ++ s + o.push(ByteVector(Headers.Array16) ++ s) } else { val s = ByteVector.fromInt(size) - ByteVector(Headers.Array32).buffer ++ s + o.push(ByteVector(Headers.Array32) ++ s) } case MsgpackItem.Map(size) => if (size <= 15) { - ByteVector.fromByte((0x80 | size).toByte) + o.push(ByteVector.fromByte((0x80 | size).toByte)) } else if (size <= Math.pow(2, 16) - 1) { val s = ByteVector.fromShort(size.toShort) - ByteVector(Headers.Map16).buffer ++ s + o.push(ByteVector(Headers.Map16) ++ s) } else { val s = ByteVector.fromInt(size) - ByteVector(Headers.Map32).buffer ++ s + o.push(ByteVector(Headers.Map32) ++ s) } case MsgpackItem.Extension(tpe, bytes) => val bs = bytes.dropWhile(_ == 0) if (bs.size <= 1) { - (ByteVector(Headers.FixExt1).buffer :+ tpe) ++ bs.padLeft(1) + o.push((ByteVector(Headers.FixExt1) :+ tpe) ++ bs.padLeft(1)) } else if (bs.size <= 2) { - (ByteVector(Headers.FixExt2).buffer :+ tpe) ++ bs.padLeft(2) + o.push((ByteVector(Headers.FixExt2) :+ tpe) ++ bs.padLeft(2)) } else if (bs.size <= 4) { - (ByteVector(Headers.FixExt4).buffer :+ tpe) ++ bs.padLeft(4) + o.push((ByteVector(Headers.FixExt4) :+ tpe) ++ bs.padLeft(4)) } else if (bs.size <= 8) { - (ByteVector(Headers.FixExt8).buffer :+ tpe) ++ bs.padLeft(8) + o.push((ByteVector(Headers.FixExt8) :+ tpe) ++ bs.padLeft(8)) } else if (bs.size <= 16) { - (ByteVector(Headers.FixExt16).buffer :+ tpe) ++ bs.padLeft(16) + o.push((ByteVector(Headers.FixExt16) :+ tpe) ++ bs.padLeft(16)) } else if (bs.size <= Math.pow(2, 8) - 1) { val size = ByteVector.fromByte(bs.size.toByte) - (ByteVector(Headers.Ext8).buffer ++ size :+ tpe) ++ bs + o.push((ByteVector(Headers.Ext8) ++ size :+ tpe) ++ bs) } else if (bs.size <= Math.pow(2, 16) - 1) { val size = ByteVector.fromShort(bs.size.toShort) - (ByteVector(Headers.Ext16).buffer ++ size :+ tpe) ++ bs + o.push((ByteVector(Headers.Ext16) ++ size :+ tpe) ++ bs) } else { val size = ByteVector.fromInt(bs.size.toInt) - (ByteVector(Headers.Ext32).buffer ++ size :+ tpe) ++ bs + /* Max length of ext32 (incl. type and length info) is 2^32 + 5 bytes + * which is more than Chunk can handle at once. + */ + o.pushBuffered((ByteVector(Headers.Ext32) ++ size :+ tpe) ++ bs) } case MsgpackItem.Timestamp32(seconds) => - (ByteVector(Headers.FixExt4).buffer :+ Headers.Timestamp.toByte) ++ ByteVector.fromInt(seconds) + o.push((ByteVector(Headers.FixExt4) :+ Headers.Timestamp.toByte) ++ ByteVector.fromInt(seconds)) case MsgpackItem.Timestamp64(combined) => - (ByteVector(Headers.FixExt8).buffer :+ Headers.Timestamp.toByte) ++ ByteVector.fromLong(combined) + o.push((ByteVector(Headers.FixExt8) :+ Headers.Timestamp.toByte) ++ ByteVector.fromLong(combined)) case MsgpackItem.Timestamp96(nanoseconds, seconds) => val ns = ByteVector.fromInt(nanoseconds) val s = ByteVector.fromLong(seconds) - (ByteVector(Headers.Ext8).buffer :+ 12 :+ Headers.Timestamp.toByte) ++ ns ++ s + o.push((ByteVector(Headers.Ext8) :+ 12 :+ Headers.Timestamp.toByte) ++ ns ++ s) case MsgpackItem.Nil => - ByteVector(Headers.Nil) + o.push(ByteVector(Headers.Nil)) case MsgpackItem.False => - ByteVector(Headers.False) + o.push(ByteVector(Headers.False)) case MsgpackItem.True => - ByteVector(Headers.True) + o.push(ByteVector(Headers.True)) } - def fast: MsgpackItem => ByteVector = { - case item: MsgpackItem.UnsignedInt => - ByteVector(Headers.Uint64) ++ item.bytes.padLeft(8) - - case item: MsgpackItem.SignedInt => - ByteVector(Headers.Int64) ++ item.bytes.padLeft(8) - - case item: MsgpackItem.Float32 => - ByteVector(Headers.Float32) ++ ByteVector.fromInt(java.lang.Float.floatToIntBits(item.v)) - - case item: MsgpackItem.Float64 => - ByteVector(Headers.Float64) ++ ByteVector.fromLong(java.lang.Double.doubleToLongBits(item.v)) - - case item: MsgpackItem.Str => - val size = ByteVector.fromInt(item.bytes.size.toInt) - ByteVector(Headers.Str32) ++ size ++ item.bytes - - case item: MsgpackItem.Bin => - val size = ByteVector.fromInt(item.bytes.size.toInt) - ByteVector(Headers.Bin32) ++ size ++ item.bytes - - case item: MsgpackItem.Array => - ByteVector(Headers.Array32) ++ ByteVector.fromInt(item.size) - - case item: MsgpackItem.Map => - ByteVector(Headers.Map32) ++ ByteVector.fromInt(item.size) - - case item: MsgpackItem.Extension => - val size = ByteVector.fromInt(item.bytes.size.toInt) - val t = ByteVector(item.tpe) - ByteVector(Headers.Ext32) ++ size ++ t ++ item.bytes - - case item: MsgpackItem.Timestamp32 => - ByteVector(Headers.FixExt4) ++ hex"ff" ++ ByteVector.fromInt(item.seconds) - - case item: MsgpackItem.Timestamp64 => - ByteVector(Headers.FixExt8) ++ hex"ff" ++ ByteVector.fromLong(item.combined) - - case item: MsgpackItem.Timestamp96 => - val ns = ByteVector.fromInt(item.nanoseconds) - val s = ByteVector.fromLong(item.seconds) - ByteVector(Headers.Ext8) ++ hex"0c" ++ hex"ff" ++ ns ++ s - - case MsgpackItem.Nil => - ByteVector(Headers.Nil) + private def stepChunk[F[_]: RaiseThrowable](ctx: SerializationContext[F]): Pull[F, Byte, SerializationContext[F]] = + if (ctx.idx >= ctx.chunk.size) + Pull.done.as(ctx) + else + step(ctx.out, ctx.chunk(ctx.idx)).flatMap { out => + stepChunk(SerializationContext(out, ctx.chunk, ctx.idx + 1, ctx.rest)) + } - case MsgpackItem.False => - ByteVector(Headers.False) + def pipe[F[_]: RaiseThrowable]: Pipe[F, MsgpackItem, Byte] = { stream => + def go(out: Out[F], rest: Stream[F, MsgpackItem]): Pull[F, Byte, Unit] = + rest.pull.uncons.flatMap { + case None => out.flush + case Some((chunk, rest)) => + stepChunk(SerializationContext(out, chunk, 0, rest)).flatMap { case SerializationContext(out, _, _, rest) => + go(out, rest) + } + } - case MsgpackItem.True => - ByteVector(Headers.True) + go(new Out(Chunk.empty), stream).stream } } diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala index 20cdfb49..3a61a4bd 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala @@ -63,9 +63,7 @@ private[low] object ItemValidator { Pull.pure(None) case MsgpackItem.Array(size) => - if (size < 0) - Pull.raiseError(new ValidationErrorAt(position, s"Array has a negative size ${size}")) - else if (size == 0) + if (size == 0) Pull.pure(None) else Pull.pure(Some(Expect(size, position))) diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala index 2d512faf..5c842486 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala @@ -26,33 +26,16 @@ package object low { def items[F[_]](implicit F: RaiseThrowable[F]): Pipe[F, Byte, MsgpackItem] = ItemParser.pipe[F] - /** Alias for `bytes(compressed = true, validated = true)` + /** Alias for `bytes(validated = true)` */ def toBinary[F[_]: RaiseThrowable]: Pipe[F, MsgpackItem, Byte] = - bytes(true, true) + bytes(true) - def bytes[F[_]](compressed: Boolean, validated: Boolean)(implicit - F: RaiseThrowable[F]): Pipe[F, MsgpackItem, Byte] = { in => - in - .through { if (validated) ItemValidator.simple else ItemValidator.none } - .flatMap { x => - val bytes = - if (compressed) - ItemSerializer.compressed(x) - else - ItemSerializer.fast(x) - - /* Maximum size of a `ByteVector` is bigger than the one of a `Chunk` (Long vs Int). The `Chunk.byteVector` - * function returns `Chunk.empty` if it encounters a `ByteVector` that won't fit in a `Chunk`. We have to work - * around this behaviour and explicitly check the `ByteVector` size. - */ - if (bytes.size <= Int.MaxValue) { - Stream.chunk(Chunk.byteVector(bytes)) - } else { - val (lhs, rhs) = bytes.splitAt(Int.MaxValue) - Stream.chunk(Chunk.byteVector(lhs)) ++ Stream.chunk(Chunk.byteVector(rhs)) - } - } + def bytes[F[_]: RaiseThrowable](validated: Boolean): Pipe[F, MsgpackItem, Byte] = { + if (validated) + ItemValidator.simple.andThen(ItemSerializer.pipe) + else + ItemSerializer.pipe } def validated[F[_]](implicit F: RaiseThrowable[F]): Pipe[F, MsgpackItem, MsgpackItem] = diff --git a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala index 59f19fd9..2874d658 100644 --- a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala +++ b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala @@ -25,146 +25,119 @@ import weaver._ import java.nio.charset.StandardCharsets import low.MsgpackItem -object SerializerSpec extends SimpleIOSuite { +object SerializerSpec extends SimpleIOSuite with Checkers { test("MessagePack item serializer should correctly serialize all formats") { - val cases = List( + val cases: List[(List[MsgpackItem], ByteVector)] = List( // nil, false, true - (List(MsgpackItem.Nil, MsgpackItem.False, MsgpackItem.True), hex"c0c2c3", hex"c0c2c3"), + (List(MsgpackItem.Nil, MsgpackItem.False, MsgpackItem.True), hex"c0c2c3"), // positive fixint - (List(MsgpackItem.SignedInt(hex"7b")), hex"7b", hex"0xd3000000000000007b"), + (List(MsgpackItem.SignedInt(hex"7b")), hex"7b"), // negative fixint - (List(MsgpackItem.SignedInt(hex"d6")), hex"d6", hex"0xd300000000000000d6"), + (List(MsgpackItem.SignedInt(hex"d6")), hex"d6"), // uint 8, uint 16, uint 32, uint 64 - (List(MsgpackItem.UnsignedInt(hex"ab")), hex"ccab", hex"cf00000000000000ab"), - (List(MsgpackItem.UnsignedInt(hex"abcd")), hex"cdabcd", hex"cf000000000000abcd"), - (List(MsgpackItem.UnsignedInt(hex"abcdef01")), hex"ceabcdef01", hex"cf00000000abcdef01"), - (List(MsgpackItem.UnsignedInt(hex"abcdef0123456789")), hex"cfabcdef0123456789", hex"cfabcdef0123456789"), + (List(MsgpackItem.UnsignedInt(hex"ab")), hex"ccab"), + (List(MsgpackItem.UnsignedInt(hex"abcd")), hex"cdabcd"), + (List(MsgpackItem.UnsignedInt(hex"abcdef01")), hex"ceabcdef01"), + (List(MsgpackItem.UnsignedInt(hex"abcdef0123456789")), hex"cfabcdef0123456789"), // int 8, int 16, int 32, int 64 - (List(MsgpackItem.SignedInt(hex"80")), hex"d080", hex"d30000000000000080"), - (List(MsgpackItem.SignedInt(hex"80ab")), hex"d180ab", hex"d300000000000080ab"), - (List(MsgpackItem.SignedInt(hex"80abcdef")), hex"d280abcdef", hex"d30000000080abcdef"), - (List(MsgpackItem.SignedInt(hex"80abcddef0123456")), hex"d380abcddef0123456", hex"d380abcddef0123456"), + (List(MsgpackItem.SignedInt(hex"80")), hex"d080"), + (List(MsgpackItem.SignedInt(hex"80ab")), hex"d180ab"), + (List(MsgpackItem.SignedInt(hex"80abcdef")), hex"d280abcdef"), + (List(MsgpackItem.SignedInt(hex"80abcddef0123456")), hex"d380abcddef0123456"), // float 32, float 64 - (List(MsgpackItem.Float32(0.125F)), hex"ca3e000000", hex"ca3e000000"), - (List(MsgpackItem.Float64(0.125)), hex"cb3fc0000000000000", hex"cb3fc0000000000000"), + (List(MsgpackItem.Float32(0.125F)), hex"ca3e000000"), + (List(MsgpackItem.Float64(0.125)), hex"cb3fc0000000000000"), // fixstr - (List(MsgpackItem.Str(ByteVector("abc".getBytes(StandardCharsets.UTF_8)))), - hex"a3616263", - hex"0xdb00000003616263"), + (List(MsgpackItem.Str(ByteVector("abc".getBytes(StandardCharsets.UTF_8)))), hex"a3616263"), // str 8 (List(MsgpackItem.Str(ByteVector("abcd".repeat(8).getBytes(StandardCharsets.UTF_8)))), - hex"d920" ++ ByteVector("abcd".repeat(8).getBytes(StandardCharsets.UTF_8)), - hex"db00000020" ++ ByteVector("abcd".repeat(8).getBytes(StandardCharsets.UTF_8))), + hex"d920" ++ ByteVector("abcd".repeat(8).getBytes(StandardCharsets.UTF_8))), // str 16 (List(MsgpackItem.Str(ByteVector("a".repeat(Math.pow(2, 8).toInt).getBytes(StandardCharsets.UTF_8)))), - hex"da0100" ++ ByteVector("a".repeat(Math.pow(2, 8).toInt).getBytes(StandardCharsets.UTF_8)), - hex"db00000100" ++ ByteVector("a".repeat(Math.pow(2, 8).toInt).getBytes(StandardCharsets.UTF_8))), + hex"da0100" ++ ByteVector("a".repeat(Math.pow(2, 8).toInt).getBytes(StandardCharsets.UTF_8))), // str 32 (List(MsgpackItem.Str(ByteVector("a".repeat(Math.pow(2, 16).toInt).getBytes(StandardCharsets.UTF_8)))), - hex"db00010000" ++ ByteVector("a".repeat(Math.pow(2, 16).toInt).getBytes(StandardCharsets.UTF_8)), hex"db00010000" ++ ByteVector("a".repeat(Math.pow(2, 16).toInt).getBytes(StandardCharsets.UTF_8))), // bin 8 (List(MsgpackItem.Bin(ByteVector("abcd".repeat(8).getBytes(StandardCharsets.UTF_8)))), - hex"c420" ++ ByteVector("abcd".repeat(8).getBytes(StandardCharsets.UTF_8)), - hex"c600000020" ++ ByteVector("abcd".repeat(8).getBytes(StandardCharsets.UTF_8))), + hex"c420" ++ ByteVector("abcd".repeat(8).getBytes(StandardCharsets.UTF_8))), // bin 16 (List(MsgpackItem.Bin(ByteVector("a".repeat(Math.pow(2, 8).toInt).getBytes(StandardCharsets.UTF_8)))), - hex"c50100" ++ ByteVector("a".repeat(Math.pow(2, 8).toInt).getBytes(StandardCharsets.UTF_8)), - hex"c600000100" ++ ByteVector("a".repeat(Math.pow(2, 8).toInt).getBytes(StandardCharsets.UTF_8))), + hex"c50100" ++ ByteVector("a".repeat(Math.pow(2, 8).toInt).getBytes(StandardCharsets.UTF_8))), // bin 32 (List(MsgpackItem.Bin(ByteVector("a".repeat(Math.pow(2, 16).toInt).getBytes(StandardCharsets.UTF_8)))), - hex"c600010000" ++ ByteVector("a".repeat(Math.pow(2, 16).toInt).getBytes(StandardCharsets.UTF_8)), hex"c600010000" ++ ByteVector("a".repeat(Math.pow(2, 16).toInt).getBytes(StandardCharsets.UTF_8))), // fixarray - (List(MsgpackItem.Array(0)), hex"90", hex"dd00000000"), - (List(MsgpackItem.Array(1)), hex"91", hex"dd00000001"), + (List(MsgpackItem.Array(0)), hex"90"), + (List(MsgpackItem.Array(1)), hex"91"), // array 16 - (List(MsgpackItem.Array(16)), hex"dc0010", hex"dd00000010"), + (List(MsgpackItem.Array(16)), hex"dc0010"), // array 32 - (List(MsgpackItem.Array(Math.pow(2, 16).toInt)), hex"dd00010000", hex"dd00010000"), + (List(MsgpackItem.Array(Math.pow(2, 16).toInt)), hex"dd00010000"), // fixmap - (List(MsgpackItem.Map(0)), hex"80", hex"df00000000"), - (List(MsgpackItem.Map(1)), hex"81", hex"df00000001"), + (List(MsgpackItem.Map(0)), hex"80"), + (List(MsgpackItem.Map(1)), hex"81"), // map 16 - (List(MsgpackItem.Map(16)), hex"de0010", hex"df00000010"), + (List(MsgpackItem.Map(16)), hex"de0010"), // map 32 - (List(MsgpackItem.Map(Math.pow(2, 16).toInt)), hex"df00010000", hex"df00010000"), + (List(MsgpackItem.Map(Math.pow(2, 16).toInt)), hex"df00010000"), // fixext 1 - (List(MsgpackItem.Extension(0x54.toByte, hex"ab")), hex"d454ab", hex"c90000000154ab"), + (List(MsgpackItem.Extension(0x54.toByte, hex"ab")), hex"d454ab"), // fixext 2 - (List(MsgpackItem.Extension(0x54.toByte, hex"abcd")), hex"d554abcd", hex"c90000000254abcd"), + (List(MsgpackItem.Extension(0x54.toByte, hex"abcd")), hex"d554abcd"), // fixext 4 - (List(MsgpackItem.Extension(0x54.toByte, hex"abcdef01")), hex"d654abcdef01", hex"c90000000454abcdef01"), + (List(MsgpackItem.Extension(0x54.toByte, hex"abcdef01")), hex"d654abcdef01"), // fixext 8 - (List(MsgpackItem.Extension(0x54.toByte, hex"abcdef0123456789")), - hex"d754abcdef0123456789", - hex"c90000000854abcdef0123456789"), + (List(MsgpackItem.Extension(0x54.toByte, hex"abcdef0123456789")), hex"d754abcdef0123456789"), // fixext 8 (List(MsgpackItem.Extension(0x54.toByte, hex"abcdef0123456789abcdef0123456789")), - hex"d854abcdef0123456789abcdef0123456789", - hex"c90000001054abcdef0123456789abcdef0123456789"), + hex"d854abcdef0123456789abcdef0123456789"), // ext 8 - (List(MsgpackItem.Extension(0x54, ByteVector.fill(17)(0xab))), - hex"c71154" ++ ByteVector.fill(17)(0xab), - hex"c90000001154" ++ ByteVector.fill(17)(0xab)), + (List(MsgpackItem.Extension(0x54, ByteVector.fill(17)(0xab))), hex"c71154" ++ ByteVector.fill(17)(0xab)), // ext 16 (List(MsgpackItem.Extension(0x54, ByteVector.fill(Math.pow(2, 8).toLong)(0xab))), - hex"c8010054" ++ ByteVector.fill(Math.pow(2, 8).toLong)(0xab), - hex"c90000010054" ++ ByteVector.fill(Math.pow(2, 8).toLong)(0xab)), + hex"c8010054" ++ ByteVector.fill(Math.pow(2, 8).toLong)(0xab)), // ext 32 (List(MsgpackItem.Extension(0x54, ByteVector.fill(Math.pow(2, 16).toLong)(0xab))), - hex"c90001000054" ++ ByteVector.fill(Math.pow(2, 16).toLong)(0xab), hex"c90001000054" ++ ByteVector.fill(Math.pow(2, 16).toLong)(0xab)), // timestamp 32 - (List(MsgpackItem.Timestamp32(0x0123abcd)), hex"d6ff0123abcd", hex"d6ff0123abcd"), + (List(MsgpackItem.Timestamp32(0x0123abcd)), hex"d6ff0123abcd"), // timestamp 64 - (List(MsgpackItem.Timestamp64(0x0123456789abcdefL)), hex"d7ff0123456789abcdef", hex"d7ff0123456789abcdef"), + (List(MsgpackItem.Timestamp64(0x0123456789abcdefL)), hex"d7ff0123456789abcdef"), // timestamp 96 - (List(MsgpackItem.Timestamp96(0x0123abcd, 0x0123456789abcdefL)), - hex"c70cff0123abcd0123456789abcdef", - hex"c70cff0123abcd0123456789abcdef") + (List(MsgpackItem.Timestamp96(0x0123abcd, 0x0123456789abcdefL)), hex"c70cff0123abcd0123456789abcdef") ) Stream .emits(cases) - .evalMap { case (source, compressed, fast) => - for { - e1 <- - Stream - .emits(source) - .through(low.bytes[IO](true, false)) - .compile - .fold(ByteVector.empty)(_ :+ _) - .map(expect.same(_, compressed)) - - e2 <- - Stream - .emits(source) - .through(low.bytes[IO](false, false)) - .compile - .fold(ByteVector.empty)(_ :+ _) - .map(expect.same(_, fast)) - } yield e1 and e2 + .evalMap { case (source, serialized) => + Stream + .emits(source) + .through(low.bytes[IO](false)) + .compile + .fold(ByteVector.empty)(_ :+ _) + .map(expect.same(_, serialized)) + } .compile .foldMonoid @@ -193,25 +166,25 @@ object SerializerSpec extends SimpleIOSuite { hex"918FA46461746582A662756666657282A474797065A6427566666572A4646174619401234567A474797065CCFFA35F6964B8363663316233363661333137353434376163346335343165A5696E64657800A467756964D92438666665653537302D353938312D346630362D623635382D653435383163363064373539A86973416374697665C3A762616C616E6365CB40A946956A97C84CA361676516A8657965436F6C6F72A4626C7565A46E616D65AD4D6F72746F6E204C6974746C65A761646472657373D9313933372044656172626F726E20436F7572742C204861726C656967682C204D6173736163687573657474732C2033353936AA72656769737465726564BA323032332D30382D32395431303A34353A3335202D30323A3030A86C61746974756465CB4047551159C49774A96C6F6E676974756465CBC065F94A771C970FA47461677397A54C6F72656DA3657374A86465736572756E74A54C6F72656DA46E697369A76C61626F726973A86465736572756E74A7667269656E64739382A2696400A46E616D65B04865726E616E64657A204C6172736F6E82A2696401A46E616D65AF4D616E6E696E672053617267656E7482A2696402A46E616D65AF536176616E6E6168204E65776D616E" ) - def round(data: ByteVector, compress: Boolean) = + def round(data: ByteVector) = Stream .chunk(Chunk.byteVector(data)) .through(low.items[IO]) - .through(low.bytes[IO](compress, false)) + .through(low.bytes[IO](false)) .fold(ByteVector.empty)(_ :+ _) - def process(compress: Boolean, serializerName: String) = - for { - data <- Stream.emits(cases) - pre <- round(data, compress) - processed <- round(pre, compress) - } yield { - if (processed == pre) - success - else - failure(s"${serializerName} should be fixpoint for: ${pre} but it emitted ${processed}") - } + val out = for { + data <- Stream.emits(cases) + pre <- round(data) + processed <- round(pre) + } yield { + if (processed == pre) + success + else + failure(s"Serializer should be fixpoint for ${pre} but it emitted ${processed}") + } + + out.compile.foldMonoid - (process(true, "ItemSerializer.compressed") ++ process(false, "ItemSerializer.none")).compile.foldMonoid } } From cdc4894e273b947c083c650d7f132118e5f4b106 Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Sat, 7 Sep 2024 12:27:12 +0200 Subject: [PATCH 14/33] Make `SerializerSpec` no longer extend `Checkers` --- msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala index 2874d658..55bd2791 100644 --- a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala +++ b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala @@ -25,7 +25,7 @@ import weaver._ import java.nio.charset.StandardCharsets import low.MsgpackItem -object SerializerSpec extends SimpleIOSuite with Checkers { +object SerializerSpec extends SimpleIOSuite { test("MessagePack item serializer should correctly serialize all formats") { val cases: List[(List[MsgpackItem], ByteVector)] = List( // nil, false, true From 309569e48961642af378989709a29637f377759f Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Sat, 7 Sep 2024 12:38:04 +0200 Subject: [PATCH 15/33] Make `msgpack.low` API similar to `cbor.low` API --- .../MsgPackItemSerializerBenchmarks.scala | 4 ++-- .../data/msgpack/low/internal/ItemValidator.scala | 5 +---- .../main/scala/fs2/data/msgpack/low/package.scala | 14 +++++--------- .../scala/fs2/data/msgpack/SerializerSpec.scala | 4 ++-- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemSerializerBenchmarks.scala b/benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemSerializerBenchmarks.scala index 325bb16d..caf49e34 100644 --- a/benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemSerializerBenchmarks.scala +++ b/benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemSerializerBenchmarks.scala @@ -55,7 +55,7 @@ class MsgPackItemSerializerBenchmarks { def serialize() = Stream .emits(msgpackItems) - .through(fs2.data.msgpack.low.bytes[SyncIO](false)) + .through(fs2.data.msgpack.low.toNonValidatedBinary[SyncIO]) .compile .drain .unsafeRunSync() @@ -64,7 +64,7 @@ class MsgPackItemSerializerBenchmarks { def withValidation() = Stream .emits(msgpackItems) - .through(fs2.data.msgpack.low.bytes[SyncIO](true)) + .through(fs2.data.msgpack.low.toBinary[SyncIO]) .compile .drain .unsafeRunSync() diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala index 3a61a4bd..a8ae4442 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala @@ -31,9 +31,7 @@ private[low] object ItemValidator { type ValidationContext = (Chunk[MsgpackItem], Int, Long, List[Expect]) - def none[F[_]]: Pipe[F, MsgpackItem, MsgpackItem] = in => in - - def simple[F[_]](implicit F: RaiseThrowable[F]): Pipe[F, MsgpackItem, MsgpackItem] = { in => + def pipe[F[_]](implicit F: RaiseThrowable[F]): Pipe[F, MsgpackItem, MsgpackItem] = { in => def step1(chunk: Chunk[MsgpackItem], idx: Int, position: Long): Pull[F, MsgpackItem, Option[Expect]] = chunk(idx) match { case MsgpackItem.UnsignedInt(bytes) => @@ -150,5 +148,4 @@ private[low] object ItemValidator { go(in, 0, 0, List.empty).stream } - } diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala index 5c842486..9433b1d6 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala @@ -29,15 +29,11 @@ package object low { /** Alias for `bytes(validated = true)` */ def toBinary[F[_]: RaiseThrowable]: Pipe[F, MsgpackItem, Byte] = - bytes(true) + _.through(ItemValidator.pipe).through(ItemSerializer.pipe) - def bytes[F[_]: RaiseThrowable](validated: Boolean): Pipe[F, MsgpackItem, Byte] = { - if (validated) - ItemValidator.simple.andThen(ItemSerializer.pipe) - else - ItemSerializer.pipe - } + def toNonValidatedBinary[F[_]: RaiseThrowable]: Pipe[F, MsgpackItem, Byte] = + ItemSerializer.pipe - def validated[F[_]](implicit F: RaiseThrowable[F]): Pipe[F, MsgpackItem, MsgpackItem] = - ItemValidator.simple[F] + def validate[F[_]](implicit F: RaiseThrowable[F]): Pipe[F, MsgpackItem, MsgpackItem] = + ItemValidator.pipe[F] } diff --git a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala index 55bd2791..13bff49e 100644 --- a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala +++ b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala @@ -133,7 +133,7 @@ object SerializerSpec extends SimpleIOSuite { .evalMap { case (source, serialized) => Stream .emits(source) - .through(low.bytes[IO](false)) + .through(low.toNonValidatedBinary) .compile .fold(ByteVector.empty)(_ :+ _) .map(expect.same(_, serialized)) @@ -170,7 +170,7 @@ object SerializerSpec extends SimpleIOSuite { Stream .chunk(Chunk.byteVector(data)) .through(low.items[IO]) - .through(low.bytes[IO](false)) + .through(low.toNonValidatedBinary) .fold(ByteVector.empty)(_ :+ _) val out = for { From 3d717a3ab6fcc66d6264c387f1d4e3d103f7cac0 Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Sat, 7 Sep 2024 12:53:09 +0200 Subject: [PATCH 16/33] Update msgpack serializer spec documentation --- msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala index 13bff49e..8fe00a15 100644 --- a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala +++ b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala @@ -146,7 +146,7 @@ object SerializerSpec extends SimpleIOSuite { test("MessagePack item serializer should be fixpoint for a subset of ByteVector") { /* The parser mapping ByteVector to MsgpackItem can be seen as a not injective morphism, that is, there * are many ByteVectors that will map to the same MsgpackItem. Because of this, we cannot possibly guarantee that - * `serialize(parse(bs))` is fixpoint for an arbitrary `bs`. However, currently implemented serializers *are* + * `serialize(parse(bs))` is fixpoint for an arbitrary `bs`. However, currently implemented serializer *is* * injective (if we exclude the Timestamp format family as it can be represented with Extension types) and so, we * can guarantee `serialize(parse(bs)) == bs` if `bs` is a member of a subset of ByteVector that is emitted by a * serializer. From deede3faf14a0b1a81643cb259c22bf9d5b03bbd Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Tue, 10 Sep 2024 20:08:19 +0200 Subject: [PATCH 17/33] Change `msgpack.low.toBinary` scaladoc Reflects changes made in 309569e --- msgpack/src/main/scala/fs2/data/msgpack/low/package.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala index 9433b1d6..a0558f8b 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala @@ -26,7 +26,9 @@ package object low { def items[F[_]](implicit F: RaiseThrowable[F]): Pipe[F, Byte, MsgpackItem] = ItemParser.pipe[F] - /** Alias for `bytes(validated = true)` + /** Transforms a stream of [[MsgpackItem]]s into a stream of [[Byte]]s. + * + * Will fail with an error if the stream is malformed. */ def toBinary[F[_]: RaiseThrowable]: Pipe[F, MsgpackItem, Byte] = _.through(ItemValidator.pipe).through(ItemSerializer.pipe) From 698f727589d6e41cc5613393148b1d63877df0d1 Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Tue, 10 Sep 2024 20:48:00 +0200 Subject: [PATCH 18/33] Fix msgpack doc generation --- msgpack/src/main/scala/fs2/data/msgpack/low/package.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala index a0558f8b..8d2a63e2 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala @@ -26,7 +26,7 @@ package object low { def items[F[_]](implicit F: RaiseThrowable[F]): Pipe[F, Byte, MsgpackItem] = ItemParser.pipe[F] - /** Transforms a stream of [[MsgpackItem]]s into a stream of [[Byte]]s. + /** Transforms a stream of [[MsgpackItem]]s into a stream of [[scala.Byte]]s. * * Will fail with an error if the stream is malformed. */ From 248fbc68c7e048502b72d76843a6febe1ee68f64 Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Tue, 10 Sep 2024 20:50:56 +0200 Subject: [PATCH 19/33] Add doc for `msgpack.low` public methods --- msgpack/src/main/scala/fs2/data/msgpack/low/package.scala | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala index 8d2a63e2..872dfaeb 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala @@ -23,6 +23,8 @@ import low.internal.{ItemParser, ItemSerializer, ItemValidator} /** A low-level representation of the MessagePack format. */ package object low { + /** Transforms a stream of [[scala.Byte]]s into a stream of [[MsgpackItem]]s. + */ def items[F[_]](implicit F: RaiseThrowable[F]): Pipe[F, Byte, MsgpackItem] = ItemParser.pipe[F] @@ -33,9 +35,15 @@ package object low { def toBinary[F[_]: RaiseThrowable]: Pipe[F, MsgpackItem, Byte] = _.through(ItemValidator.pipe).through(ItemSerializer.pipe) + /** Transforms a stream of [[MsgpackItem]]s into a stream of [[scala.Byte]]s. + * + * Will not validate the input stream and can potentially produce malformed data. Consider using [[toBinary]]. + */ def toNonValidatedBinary[F[_]: RaiseThrowable]: Pipe[F, MsgpackItem, Byte] = ItemSerializer.pipe + /** Validates a stream of [[MsgpackItem]]s, fails when the stream is malformed. + */ def validate[F[_]](implicit F: RaiseThrowable[F]): Pipe[F, MsgpackItem, MsgpackItem] = ItemValidator.pipe[F] } From 4760221d4df1d8c31d78264422853b5e946439a1 Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Tue, 10 Sep 2024 20:54:48 +0200 Subject: [PATCH 20/33] Run prePR --- msgpack/src/main/scala/fs2/data/msgpack/low/package.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala index 872dfaeb..46525f79 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/package.scala @@ -23,6 +23,7 @@ import low.internal.{ItemParser, ItemSerializer, ItemValidator} /** A low-level representation of the MessagePack format. */ package object low { + /** Transforms a stream of [[scala.Byte]]s into a stream of [[MsgpackItem]]s. */ def items[F[_]](implicit F: RaiseThrowable[F]): Pipe[F, Byte, MsgpackItem] = From 041e1359dc50407a7f48a4091edc06296686abd2 Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Sat, 14 Sep 2024 12:07:00 +0200 Subject: [PATCH 21/33] Extract literals into constants --- .../msgpack/low/internal/ItemSerializer.scala | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala index 09bbdf6d..cfb9230e 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala @@ -29,6 +29,13 @@ private[low] object ItemSerializer { class MalformedIntError extends MalformedItemError class MalformedUintError extends MalformedItemError + private final val positiveIntMask = hex"7f" + private final val negativeIntMask = hex"e0" + + private final val mapMask = 0x80 + private final val arrayMask = 0x90 + private final val strMask = 0xa0 + /** Checks whether integer `x` fits in `n` bytes. */ @inline private def fitsIn(x: Int, n: Long): Boolean = @@ -100,7 +107,7 @@ private[low] object ItemSerializer { val bs = bytes.dropWhile(_ == 0) if (bs.size <= 1) // positive fixint or negative fixint - if ((bs & hex"7f") == bs || (bs & hex"c0") == hex"c0") + if ((bs & positiveIntMask) == bs || (bs & negativeIntMask) == negativeIntMask) o.push(bs.padLeft(1)) else o.push(ByteVector(Headers.Int8) ++ bs.padLeft(1)) @@ -121,7 +128,7 @@ private[low] object ItemSerializer { case MsgpackItem.Str(bytes) => if (bytes.size <= 31) { - o.push(ByteVector.fromByte((0xa0 | bytes.size).toByte) ++ bytes) + o.push(ByteVector.fromByte((strMask | bytes.size).toByte) ++ bytes) } else if (bytes.size <= Math.pow(2, 8) - 1) { val size = ByteVector.fromByte(bytes.size.toByte) o.push(ByteVector(Headers.Str8) ++ size ++ bytes) @@ -157,7 +164,7 @@ private[low] object ItemSerializer { case MsgpackItem.Array(size) => if (fitsIn(size, 4)) { - o.push(ByteVector.fromByte((0x90 | size).toByte)) + o.push(ByteVector.fromByte((arrayMask | size).toByte)) } else if (size <= Math.pow(2, 16) - 1) { val s = ByteVector.fromShort(size.toShort) o.push(ByteVector(Headers.Array16) ++ s) @@ -168,7 +175,7 @@ private[low] object ItemSerializer { case MsgpackItem.Map(size) => if (size <= 15) { - o.push(ByteVector.fromByte((0x80 | size).toByte)) + o.push(ByteVector.fromByte((mapMask | size).toByte)) } else if (size <= Math.pow(2, 16) - 1) { val s = ByteVector.fromShort(size.toShort) o.push(ByteVector(Headers.Map16) ++ s) From 2be8831a29bf2669eb7a55412e044bc1bbdece6a Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Sat, 14 Sep 2024 12:08:33 +0200 Subject: [PATCH 22/33] Fix msgpack serialization test of negative fixint The serializer itself was corrected in 041e1359dc50407a7f48a4091edc06296686abd2 --- msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala index 8fe00a15..33025a78 100644 --- a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala +++ b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala @@ -34,7 +34,7 @@ object SerializerSpec extends SimpleIOSuite { // positive fixint (List(MsgpackItem.SignedInt(hex"7b")), hex"7b"), // negative fixint - (List(MsgpackItem.SignedInt(hex"d6")), hex"d6"), + (List(MsgpackItem.SignedInt(hex"e6")), hex"e6"), // uint 8, uint 16, uint 32, uint 64 (List(MsgpackItem.UnsignedInt(hex"ab")), hex"ccab"), From fd845e87b9cf5b6c00b6d37e2110881bb6c1da6d Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Sat, 21 Sep 2024 16:14:42 +0200 Subject: [PATCH 23/33] Make msgpack Array and Map use Long for sizes MessagePack Arrays and Maps can hold up to 2^32 - 1 items which is more than the `Int` type can represent without negative values. --- .../fs2/data/msgpack/low/internal/FormatParsers.scala | 4 ++-- .../fs2/data/msgpack/low/internal/ItemParser.scala | 4 ++-- .../fs2/data/msgpack/low/internal/ItemSerializer.scala | 6 +++--- .../fs2/data/msgpack/low/internal/ItemValidator.scala | 10 ++++++++-- .../src/main/scala/fs2/data/msgpack/low/model.scala | 4 ++-- .../test/scala/fs2/data/msgpack/SerializerSpec.scala | 4 ++-- 6 files changed, 19 insertions(+), 13 deletions(-) diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/FormatParsers.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/FormatParsers.scala index b568a6f7..dde7b2af 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/FormatParsers.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/FormatParsers.scala @@ -34,14 +34,14 @@ private[internal] object FormatParsers { def parseArray[F[_]](length: Int, ctx: ParserContext[F])(implicit F: RaiseThrowable[F]): Pull[F, MsgpackItem, ParserContext[F]] = { requireBytes(length, ctx).map { res => - res.accumulate(v => MsgpackItem.Array(v.toInt(false, ByteOrdering.BigEndian))) + res.accumulate(v => MsgpackItem.Array(v.toLong(false))) } } def parseMap[F[_]](length: Int, ctx: ParserContext[F])(implicit F: RaiseThrowable[F]): Pull[F, MsgpackItem, ParserContext[F]] = { requireBytes(length, ctx).map { res => - res.accumulate(v => MsgpackItem.Map(v.toInt(false, ByteOrdering.BigEndian))) + res.accumulate(v => MsgpackItem.Map(v.toLong(false))) } } diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemParser.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemParser.scala index 4536d936..cc04016b 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemParser.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemParser.scala @@ -77,13 +77,13 @@ private[low] object ItemParser { // fixmap else if ((byte & 0xf0) == 0x80) { val length = byte & 0x0f // 0x8f- 0x80 - Pull.pure(ctx.prepend(MsgpackItem.Map(length))) + Pull.pure(ctx.prepend(MsgpackItem.Map(length.toLong))) } // fixarray else if ((byte & 0xf0) == 0x90) { val length = byte & 0x0f // 0x9f- 0x90 - Pull.pure(ctx.prepend(MsgpackItem.Array(length))) + Pull.pure(ctx.prepend(MsgpackItem.Array(length.toLong))) } // fixstr diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala index cfb9230e..5d471dd2 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala @@ -163,13 +163,13 @@ private[low] object ItemSerializer { } case MsgpackItem.Array(size) => - if (fitsIn(size, 4)) { + if (size <= 15) { o.push(ByteVector.fromByte((arrayMask | size).toByte)) } else if (size <= Math.pow(2, 16) - 1) { val s = ByteVector.fromShort(size.toShort) o.push(ByteVector(Headers.Array16) ++ s) } else { - val s = ByteVector.fromInt(size) + val s = ByteVector.fromLong(size, 4) o.push(ByteVector(Headers.Array32) ++ s) } @@ -180,7 +180,7 @@ private[low] object ItemSerializer { val s = ByteVector.fromShort(size.toShort) o.push(ByteVector(Headers.Map16) ++ s) } else { - val s = ByteVector.fromInt(size) + val s = ByteVector.fromLong(size, 4) o.push(ByteVector(Headers.Map32) ++ s) } diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala index a8ae4442..b961568a 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala @@ -25,7 +25,7 @@ case class ValidationError(msg: String) extends Exception(msg) private[low] object ItemValidator { - case class Expect(n: Int, from: Long) { + case class Expect(n: Long, from: Long) { def dec = Expect(n - 1, from) } @@ -61,7 +61,11 @@ private[low] object ItemValidator { Pull.pure(None) case MsgpackItem.Array(size) => - if (size == 0) + if (size < 0) + Pull.raiseError(new ValidationErrorAt(position, s"Array has a negative size ${size}")) + else if (size >= (1L << 32)) + Pull.raiseError(new ValidationErrorAt(position, s"Array size exceeds (2^32)-1")) + else if (size == 0) Pull.pure(None) else Pull.pure(Some(Expect(size, position))) @@ -69,6 +73,8 @@ private[low] object ItemValidator { case MsgpackItem.Map(size) => if (size < 0) Pull.raiseError(new ValidationErrorAt(position, s"Map has a negative size ${size}")) + else if (size >= (1L << 32)) + Pull.raiseError(new ValidationErrorAt(position, s"Map size exceeds (2^32)-1")) else if (size == 0) Pull.pure(None) else diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/model.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/model.scala index 675a45b7..e191d7a0 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/model.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/model.scala @@ -34,8 +34,8 @@ object MsgpackItem { case class Str(bytes: ByteVector) extends MsgpackItem case class Bin(bytes: ByteVector) extends MsgpackItem - case class Array(size: Int) extends MsgpackItem - case class Map(size: Int) extends MsgpackItem + case class Array(size: Long) extends MsgpackItem + case class Map(size: Long) extends MsgpackItem case class Extension(tpe: Byte, bytes: ByteVector) extends MsgpackItem diff --git a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala index 33025a78..79a889b5 100644 --- a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala +++ b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala @@ -85,7 +85,7 @@ object SerializerSpec extends SimpleIOSuite { // array 16 (List(MsgpackItem.Array(16)), hex"dc0010"), // array 32 - (List(MsgpackItem.Array(Math.pow(2, 16).toInt)), hex"dd00010000"), + (List(MsgpackItem.Array(Math.pow(2, 16).toLong)), hex"dd00010000"), // fixmap (List(MsgpackItem.Map(0)), hex"80"), @@ -93,7 +93,7 @@ object SerializerSpec extends SimpleIOSuite { // map 16 (List(MsgpackItem.Map(16)), hex"de0010"), // map 32 - (List(MsgpackItem.Map(Math.pow(2, 16).toInt)), hex"df00010000"), + (List(MsgpackItem.Map(Math.pow(2, 16).toLong)), hex"df00010000"), // fixext 1 (List(MsgpackItem.Extension(0x54.toByte, hex"ab")), hex"d454ab"), From 482bf9e5fbbe4bf9cf244d1fa2fc0d6b081ed598 Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Sun, 22 Sep 2024 15:22:46 +0200 Subject: [PATCH 24/33] Make msgpack exceptions public --- .../scala/fs2/data/msgpack/exceptions.scala | 32 +++++++++ .../msgpack/low/internal/FormatParsers.scala | 2 +- .../data/msgpack/low/internal/Helpers.scala | 5 +- .../msgpack/low/internal/ItemParser.scala | 4 +- .../msgpack/low/internal/ItemSerializer.scala | 22 +++---- .../msgpack/low/internal/ItemValidator.scala | 29 ++++---- .../fs2/data/msgpack/ValidationSpec.scala | 66 +++++++++---------- 7 files changed, 93 insertions(+), 67 deletions(-) create mode 100644 msgpack/src/main/scala/fs2/data/msgpack/exceptions.scala diff --git a/msgpack/src/main/scala/fs2/data/msgpack/exceptions.scala b/msgpack/src/main/scala/fs2/data/msgpack/exceptions.scala new file mode 100644 index 00000000..d4940a68 --- /dev/null +++ b/msgpack/src/main/scala/fs2/data/msgpack/exceptions.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2024 fs2-data Project + * + * 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 fs2 +package data +package msgpack + +abstract class MsgpackException(msg: String, cause: Throwable = null) extends Exception(msg, cause) + +case class MsgpackMalformedItemException(msg: String, position: Option[Long] = None, inner: Throwable = null) + extends MsgpackException(position.fold(msg)(pos => s"at position $pos"), inner) + +case class MsgpackUnexpectedEndOfStreamException(position: Option[Long] = None, inner: Throwable = null) + extends MsgpackException( + position.fold("Unexpected end of stream")(pos => s"Unexpected end of stream starting at position $pos"), + inner) + +case class MsgpackMalformedByteStreamException(msg: String, inner: Throwable = null) + extends MsgpackException(msg, inner) diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/FormatParsers.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/FormatParsers.scala index dde7b2af..896cf121 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/FormatParsers.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/FormatParsers.scala @@ -63,7 +63,7 @@ private[internal] object FormatParsers { res <- requireBytes(8, res.toContext) seconds = res.result.toLong(false) } yield res.toContext.prepend(MsgpackItem.Timestamp96(nanosec, seconds)) - case _ => Pull.raiseError(new MsgpackParsingException(s"Invalid timestamp length: ${length}")) + case _ => Pull.raiseError(MsgpackMalformedByteStreamException(s"Invalid timestamp length: ${length}")) } } diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/Helpers.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/Helpers.scala index 12a884cb..881c81ce 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/Helpers.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/Helpers.scala @@ -23,7 +23,6 @@ package internal import scodec.bits.ByteVector private[internal] object Helpers { - case class MsgpackParsingException(str: String) extends Exception /** @param chunk Current chunk * @param idx Index of the current [[Byte]] in `chunk` @@ -67,7 +66,7 @@ private[internal] object Helpers { // Inbounds chunk access is guaranteed by `ensureChunk` Pull.pure(ctx.next.toResult(ctx.chunk(ctx.idx))) } { - Pull.raiseError(new MsgpackParsingException("Unexpected end of input")) + Pull.raiseError(MsgpackUnexpectedEndOfStreamException()) } } @@ -93,7 +92,7 @@ private[internal] object Helpers { go(count - available, ParserContext(chunk, slice.size, rest, acc), newBytes) } } { - Pull.raiseError(new MsgpackParsingException("Unexpected end of input")) + Pull.raiseError(MsgpackUnexpectedEndOfStreamException()) } } diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemParser.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemParser.scala index cc04016b..f411a48a 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemParser.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemParser.scala @@ -37,7 +37,7 @@ private[low] object ItemParser { ((byte & 0xff): @switch) match { case Headers.Nil => Pull.pure(ctx.prepend(MsgpackItem.Nil)) - case Headers.NeverUsed => Pull.raiseError(new MsgpackParsingException("Reserved value 0xc1 used")) + case Headers.NeverUsed => Pull.raiseError(MsgpackMalformedByteStreamException("Reserved value 0xc1 used")) case Headers.False => Pull.pure(ctx.prepend(MsgpackItem.False)) case Headers.True => Pull.pure(ctx.prepend(MsgpackItem.True)) case Headers.Bin8 => parseBin(1, ctx) @@ -98,7 +98,7 @@ private[low] object ItemParser { else if ((byte & 0xe0) == 0xe0) { Pull.pure(ctx.prepend(MsgpackItem.SignedInt(ByteVector(byte)))) } else { - Pull.raiseError(new MsgpackParsingException(s"Invalid type ${byte}")) + Pull.raiseError(MsgpackMalformedByteStreamException(s"Invalid type ${byte}")) } } } diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala index 5d471dd2..bb405163 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala @@ -23,12 +23,6 @@ package internal import scodec.bits._ private[low] object ItemSerializer { - class MalformedItemError extends Error("item exceeds the maximum size of it's format") - class MalformedStringError extends MalformedItemError - class MalformedBinError extends MalformedItemError - class MalformedIntError extends MalformedItemError - class MalformedUintError extends MalformedItemError - private final val positiveIntMask = hex"7f" private final val negativeIntMask = hex"e0" @@ -101,7 +95,7 @@ private[low] object ItemSerializer { else if (bs.size <= 8) o.push(ByteVector(Headers.Uint64) ++ bs.padLeft(8)) else - Pull.raiseError(new MalformedUintError) + Pull.raiseError(MsgpackMalformedItemException("Unsigned int exceeds 64 bits")) case MsgpackItem.SignedInt(bytes) => val bs = bytes.dropWhile(_ == 0) @@ -118,7 +112,7 @@ private[low] object ItemSerializer { else if (bs.size <= 8) o.push(ByteVector(Headers.Int64) ++ bs.padLeft(8)) else - Pull.raiseError(new MalformedIntError) + Pull.raiseError(MsgpackMalformedItemException("Signed int exceeds 64 bits")) case MsgpackItem.Float32(float) => o.push(ByteVector(Headers.Float32) ++ ByteVector.fromInt(java.lang.Float.floatToIntBits(float))) @@ -142,7 +136,7 @@ private[low] object ItemSerializer { */ o.pushBuffered(ByteVector(Headers.Str32) ++ size ++ bytes) } else { - Pull.raiseError(new MalformedStringError) + Pull.raiseError(MsgpackMalformedItemException("String exceeds (2^32)-1 bytes")) } case MsgpackItem.Bin(bytes) => @@ -159,7 +153,7 @@ private[low] object ItemSerializer { */ o.pushBuffered(ByteVector(Headers.Bin32) ++ size ++ bytes) } else { - Pull.raiseError(new MalformedBinError) + Pull.raiseError(MsgpackMalformedItemException("Binary data exceeds (2^32)-1 bytes")) } case MsgpackItem.Array(size) => @@ -168,9 +162,11 @@ private[low] object ItemSerializer { } else if (size <= Math.pow(2, 16) - 1) { val s = ByteVector.fromShort(size.toShort) o.push(ByteVector(Headers.Array16) ++ s) - } else { + } else if (size <= (1L << 32) - 1) { val s = ByteVector.fromLong(size, 4) o.push(ByteVector(Headers.Array32) ++ s) + } else { + Pull.raiseError(MsgpackMalformedItemException("Array size exceeds (2^32)-1")) } case MsgpackItem.Map(size) => @@ -179,9 +175,11 @@ private[low] object ItemSerializer { } else if (size <= Math.pow(2, 16) - 1) { val s = ByteVector.fromShort(size.toShort) o.push(ByteVector(Headers.Map16) ++ s) - } else { + } else if (size <= (1L << 32) - 1) { val s = ByteVector.fromLong(size, 4) o.push(ByteVector(Headers.Map32) ++ s) + } else { + Pull.raiseError(MsgpackMalformedItemException("Map size exceeds (2^32)-1 pairs")) } case MsgpackItem.Extension(tpe, bytes) => diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala index b961568a..dfbaade4 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala @@ -20,9 +20,6 @@ package msgpack package low package internal -case class ValidationErrorAt(at: Long, msg: String) extends Error(s"at position ${at}: ${msg}") -case class ValidationError(msg: String) extends Exception(msg) - private[low] object ItemValidator { case class Expect(n: Long, from: Long) { @@ -35,11 +32,13 @@ private[low] object ItemValidator { def step1(chunk: Chunk[MsgpackItem], idx: Int, position: Long): Pull[F, MsgpackItem, Option[Expect]] = chunk(idx) match { case MsgpackItem.UnsignedInt(bytes) => - if (bytes.size > 8) Pull.raiseError(new ValidationErrorAt(position, "Unsigned int exceeds 64 bits")) + if (bytes.size > 8) + Pull.raiseError(MsgpackMalformedItemException("Unsigned int exceeds 64 bits", Some(position))) else Pull.pure(None) case MsgpackItem.SignedInt(bytes) => - if (bytes.size > 8) Pull.raiseError(new ValidationErrorAt(position, "Signed int exceeds 64 bits")) + if (bytes.size > 8) + Pull.raiseError(MsgpackMalformedItemException("Signed int exceeds 64 bits", Some(position))) else Pull.pure(None) case MsgpackItem.Float32(_) => @@ -50,21 +49,21 @@ private[low] object ItemValidator { case MsgpackItem.Str(bytes) => if (bytes.size > Math.pow(2, 32) - 1) - Pull.raiseError(new ValidationErrorAt(position, "String exceeds (2^32)-1 bytes")) + Pull.raiseError(MsgpackMalformedItemException("String exceeds (2^32)-1 bytes", Some(position))) else Pull.pure(None) case MsgpackItem.Bin(bytes) => if (bytes.size > Math.pow(2, 32) - 1) - Pull.raiseError(new ValidationErrorAt(position, "Bin exceeds (2^32)-1 bytes")) + Pull.raiseError(MsgpackMalformedItemException("Bin exceeds (2^32)-1 bytes", Some(position))) else Pull.pure(None) case MsgpackItem.Array(size) => if (size < 0) - Pull.raiseError(new ValidationErrorAt(position, s"Array has a negative size ${size}")) + Pull.raiseError(MsgpackMalformedItemException(s"Array has a negative size ${size}", Some(position))) else if (size >= (1L << 32)) - Pull.raiseError(new ValidationErrorAt(position, s"Array size exceeds (2^32)-1")) + Pull.raiseError(MsgpackMalformedItemException(s"Array size exceeds (2^32)-1", Some(position))) else if (size == 0) Pull.pure(None) else @@ -72,9 +71,9 @@ private[low] object ItemValidator { case MsgpackItem.Map(size) => if (size < 0) - Pull.raiseError(new ValidationErrorAt(position, s"Map has a negative size ${size}")) + Pull.raiseError(MsgpackMalformedItemException(s"Map has a negative size ${size}", Some(position))) else if (size >= (1L << 32)) - Pull.raiseError(new ValidationErrorAt(position, s"Map size exceeds (2^32)-1")) + Pull.raiseError(MsgpackMalformedItemException(s"Map size exceeds (2^32)-1", Some(position))) else if (size == 0) Pull.pure(None) else @@ -82,7 +81,7 @@ private[low] object ItemValidator { case MsgpackItem.Extension(_, bytes) => if (bytes.size > Math.pow(2, 32) - 1) - Pull.raiseError(new ValidationErrorAt(position, "Extension data exceeds (2^32)-1 bytes")) + Pull.raiseError(MsgpackMalformedItemException("Extension data exceeds (2^32)-1 bytes", Some(position))) else Pull.pure(None) @@ -92,14 +91,14 @@ private[low] object ItemValidator { case item: MsgpackItem.Timestamp64 => if (item.nanoseconds > 999999999) Pull.raiseError( - new ValidationErrorAt(position, "Timestamp64 nanoseconds cannot be larger than '999999999'")) + MsgpackMalformedItemException("Timestamp64 nanoseconds is larger than '999999999'", Some(position))) else Pull.pure(None) case MsgpackItem.Timestamp96(nanoseconds, _) => if (nanoseconds > 999999999) Pull.raiseError( - new ValidationErrorAt(position, "Timestamp96 nanoseconds cannot be larger than '999999999'")) + MsgpackMalformedItemException("Timestamp96 nanoseconds is larger than '999999999'", Some(position))) else Pull.pure(None) @@ -149,7 +148,7 @@ private[low] object ItemValidator { if (state.isEmpty) Pull.done else - Pull.raiseError(new ValidationError(s"Unexpected end of input (starting at ${state.head.from})")) + Pull.raiseError(MsgpackUnexpectedEndOfStreamException(Some(state.head.from))) } go(in, 0, 0, List.empty).stream diff --git a/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala index 9ef00a2e..5edd065a 100644 --- a/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala +++ b/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala @@ -20,11 +20,9 @@ package msgpack import cats.effect._ import low.MsgpackItem -import fs2.data.msgpack.low.internal.{ValidationError, ValidationErrorAt} import scodec.bits.ByteVector import weaver._ import scodec.bits._ - import cats.implicits._ object ValidationSpec extends SimpleIOSuite { @@ -60,58 +58,58 @@ object ValidationSpec extends SimpleIOSuite { test("should raise if integer values exceed 64 bits") { validation1( - MsgpackItem.UnsignedInt(hex"10000000000000000") -> new ValidationErrorAt(0, "Unsigned int exceeds 64 bits"), - MsgpackItem.SignedInt(hex"10000000000000000") -> new ValidationErrorAt(0, "Signed int exceeds 64 bits") + MsgpackItem.UnsignedInt(hex"10000000000000000") -> + MsgpackMalformedItemException("Unsigned int exceeds 64 bits", Some(0)), + MsgpackItem.SignedInt(hex"10000000000000000") -> + MsgpackMalformedItemException("Signed int exceeds 64 bits", Some(0)) ) } test("should raise if string or binary values exceed 2^32 - 1 bytes") { validation1( - MsgpackItem.Str(ByteVector.empty.padLeft(Math.pow(2, 32).toLong)) -> new ValidationErrorAt( - 0, - "String exceeds (2^32)-1 bytes"), - MsgpackItem.Bin(ByteVector.empty.padLeft(Math.pow(2, 32).toLong)) -> new ValidationErrorAt( - 0, - "Bin exceeds (2^32)-1 bytes") + MsgpackItem.Str(ByteVector.empty.padLeft(Math.pow(2, 32).toLong)) -> + MsgpackMalformedItemException("String exceeds (2^32)-1 bytes", Some(0)), + MsgpackItem.Bin(ByteVector.empty.padLeft(Math.pow(2, 32).toLong)) -> + MsgpackMalformedItemException("Bin exceeds (2^32)-1 bytes", Some(0)) ) } test("should raise on unexpected end of input") { validation( - List(MsgpackItem.Array(2), MsgpackItem.True) -> new ValidationError("Unexpected end of input (starting at 0)"), - List(MsgpackItem.Array(2), MsgpackItem.Array(1), MsgpackItem.True) -> new ValidationError( - "Unexpected end of input (starting at 0)"), - List(MsgpackItem.Array(1), MsgpackItem.Array(1)) -> new ValidationError( - "Unexpected end of input (starting at 1)"), - List(MsgpackItem.Array(0), MsgpackItem.Array(1)) -> new ValidationError( - "Unexpected end of input (starting at 1)"), - List(MsgpackItem.Map(1), MsgpackItem.True) -> new ValidationError("Unexpected end of input (starting at 0)"), - List(MsgpackItem.Map(1), MsgpackItem.Map(1), MsgpackItem.True, MsgpackItem.True) -> new ValidationError( - "Unexpected end of input (starting at 0)"), - List(MsgpackItem.Map(2), MsgpackItem.True, MsgpackItem.Map(1)) -> new ValidationError( - "Unexpected end of input (starting at 2)"), - List(MsgpackItem.Map(2), MsgpackItem.True, MsgpackItem.Map(1)) -> new ValidationError( - "Unexpected end of input (starting at 2)"), - List(MsgpackItem.Map(0), MsgpackItem.Map(1)) -> new ValidationError("Unexpected end of input (starting at 1)") + List(MsgpackItem.Array(2), MsgpackItem.True) -> + MsgpackUnexpectedEndOfStreamException(Some(0)), + List(MsgpackItem.Array(2), MsgpackItem.Array(1), MsgpackItem.True) -> + MsgpackUnexpectedEndOfStreamException(Some(0)), + List(MsgpackItem.Array(1), MsgpackItem.Array(1)) -> + MsgpackUnexpectedEndOfStreamException(Some(1)), + List(MsgpackItem.Array(0), MsgpackItem.Array(1)) -> + MsgpackUnexpectedEndOfStreamException(Some(1)), + List(MsgpackItem.Map(1), MsgpackItem.True) -> + MsgpackUnexpectedEndOfStreamException(Some(0)), + List(MsgpackItem.Map(1), MsgpackItem.Map(1), MsgpackItem.True, MsgpackItem.True) -> + MsgpackUnexpectedEndOfStreamException(Some(0)), + List(MsgpackItem.Map(2), MsgpackItem.True, MsgpackItem.Map(1)) -> + MsgpackUnexpectedEndOfStreamException(Some(2)), + List(MsgpackItem.Map(2), MsgpackItem.True, MsgpackItem.Map(1)) -> + MsgpackUnexpectedEndOfStreamException(Some(2)), + List(MsgpackItem.Map(0), MsgpackItem.Map(1)) -> + MsgpackUnexpectedEndOfStreamException(Some(1)) ) } test("should raise if extension data exceeds 2^32 - 1 bytes") { validation1( - MsgpackItem.Extension(0x54, ByteVector.empty.padLeft(Math.pow(2, 32).toLong)) -> new ValidationErrorAt( - 0, - "Extension data exceeds (2^32)-1 bytes") + MsgpackItem.Extension(0x54, ByteVector.empty.padLeft(Math.pow(2, 32).toLong)) -> + MsgpackMalformedItemException("Extension data exceeds (2^32)-1 bytes", Some(0)) ) } test("should raise if nanoseconds fields exceed 999999999") { validation1( - MsgpackItem.Timestamp64(0xee6b280000000000L) -> new ValidationErrorAt( - 0, - "Timestamp64 nanoseconds cannot be larger than '999999999'"), - MsgpackItem.Timestamp96(1000000000, 0) -> new ValidationErrorAt( - 0, - "Timestamp96 nanoseconds cannot be larger than '999999999'") + MsgpackItem.Timestamp64(0xee6b280000000000L) -> + MsgpackMalformedItemException("Timestamp64 nanoseconds is larger than '999999999'", Some(0)), + MsgpackItem.Timestamp96(1000000000, 0) -> + MsgpackMalformedItemException("Timestamp96 nanoseconds is larger than '999999999'", Some(0)) ) } } From 8d677682b726c3fb7c5d1f9cf0a0e76ce69f53fc Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Sun, 22 Sep 2024 16:50:06 +0200 Subject: [PATCH 25/33] Move Pull.pure(None) into a constant --- .../msgpack/low/internal/ItemValidator.scala | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala index dfbaade4..c1f27043 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala @@ -26,6 +26,8 @@ private[low] object ItemValidator { def dec = Expect(n - 1, from) } + private val PullNone = Pull.pure(None) + type ValidationContext = (Chunk[MsgpackItem], Int, Long, List[Expect]) def pipe[F[_]](implicit F: RaiseThrowable[F]): Pipe[F, MsgpackItem, MsgpackItem] = { in => @@ -34,30 +36,30 @@ private[low] object ItemValidator { case MsgpackItem.UnsignedInt(bytes) => if (bytes.size > 8) Pull.raiseError(MsgpackMalformedItemException("Unsigned int exceeds 64 bits", Some(position))) - else Pull.pure(None) + else PullNone case MsgpackItem.SignedInt(bytes) => if (bytes.size > 8) Pull.raiseError(MsgpackMalformedItemException("Signed int exceeds 64 bits", Some(position))) - else Pull.pure(None) + else PullNone case MsgpackItem.Float32(_) => - Pull.pure(None) + PullNone case MsgpackItem.Float64(_) => - Pull.pure(None) + PullNone case MsgpackItem.Str(bytes) => if (bytes.size > Math.pow(2, 32) - 1) Pull.raiseError(MsgpackMalformedItemException("String exceeds (2^32)-1 bytes", Some(position))) else - Pull.pure(None) + PullNone case MsgpackItem.Bin(bytes) => if (bytes.size > Math.pow(2, 32) - 1) Pull.raiseError(MsgpackMalformedItemException("Bin exceeds (2^32)-1 bytes", Some(position))) else - Pull.pure(None) + PullNone case MsgpackItem.Array(size) => if (size < 0) @@ -65,7 +67,7 @@ private[low] object ItemValidator { else if (size >= (1L << 32)) Pull.raiseError(MsgpackMalformedItemException(s"Array size exceeds (2^32)-1", Some(position))) else if (size == 0) - Pull.pure(None) + PullNone else Pull.pure(Some(Expect(size, position))) @@ -75,7 +77,7 @@ private[low] object ItemValidator { else if (size >= (1L << 32)) Pull.raiseError(MsgpackMalformedItemException(s"Map size exceeds (2^32)-1", Some(position))) else if (size == 0) - Pull.pure(None) + PullNone else Pull.pure(Some(Expect(size * 2, position))) @@ -83,33 +85,33 @@ private[low] object ItemValidator { if (bytes.size > Math.pow(2, 32) - 1) Pull.raiseError(MsgpackMalformedItemException("Extension data exceeds (2^32)-1 bytes", Some(position))) else - Pull.pure(None) + PullNone case _: MsgpackItem.Timestamp32 => - Pull.pure(None) + PullNone case item: MsgpackItem.Timestamp64 => if (item.nanoseconds > 999999999) Pull.raiseError( MsgpackMalformedItemException("Timestamp64 nanoseconds is larger than '999999999'", Some(position))) else - Pull.pure(None) + PullNone case MsgpackItem.Timestamp96(nanoseconds, _) => if (nanoseconds > 999999999) Pull.raiseError( MsgpackMalformedItemException("Timestamp96 nanoseconds is larger than '999999999'", Some(position))) else - Pull.pure(None) + PullNone case MsgpackItem.Nil => - Pull.pure(None) + PullNone case MsgpackItem.True => - Pull.pure(None) + PullNone case MsgpackItem.False => - Pull.pure(None) + PullNone } def stepChunk(chunk: Chunk[MsgpackItem], From 05c4c1cc614a114dff8243276a95a11b5da2bf9c Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Sun, 22 Sep 2024 16:52:14 +0200 Subject: [PATCH 26/33] Use bit shifts instead of `Math.pow(2, n)` Also drop the `fitsIn` function as we now use `Long`s instead of `Int`s and so we don't need to compare unsigned values. --- .../msgpack/low/internal/ItemSerializer.scala | 25 ++++++++----------- .../msgpack/low/internal/ItemValidator.scala | 6 ++--- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala index bb405163..8cd09b8b 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala @@ -30,11 +30,6 @@ private[low] object ItemSerializer { private final val arrayMask = 0x90 private final val strMask = 0xa0 - /** Checks whether integer `x` fits in `n` bytes. */ - @inline - private def fitsIn(x: Int, n: Long): Boolean = - java.lang.Integer.compareUnsigned(x, (Math.pow(2, n.toDouble).toLong - 1).toInt) <= 0 - private case class SerializationContext[F[_]](out: Out[F], chunk: Chunk[MsgpackItem], idx: Int, @@ -123,13 +118,13 @@ private[low] object ItemSerializer { case MsgpackItem.Str(bytes) => if (bytes.size <= 31) { o.push(ByteVector.fromByte((strMask | bytes.size).toByte) ++ bytes) - } else if (bytes.size <= Math.pow(2, 8) - 1) { + } else if (bytes.size <= (1 << 8) - 1) { val size = ByteVector.fromByte(bytes.size.toByte) o.push(ByteVector(Headers.Str8) ++ size ++ bytes) - } else if (bytes.size <= Math.pow(2, 16) - 1) { + } else if (bytes.size <= (1 << 16) - 1) { val size = ByteVector.fromShort(bytes.size.toShort) o.push(ByteVector(Headers.Str16) ++ size ++ bytes) - } else if (fitsIn(bytes.size.toInt, 32)) { + } else if (bytes.size <= (1L << 32) - 1) { val size = ByteVector.fromInt(bytes.size.toInt) /* Max length of str32 (incl. type and length info) is 2^32 + 4 bytes * which is more than Chunk can handle at once @@ -140,13 +135,13 @@ private[low] object ItemSerializer { } case MsgpackItem.Bin(bytes) => - if (bytes.size <= Math.pow(2, 8) - 1) { + if (bytes.size <= (1 << 8) - 1) { val size = ByteVector.fromByte(bytes.size.toByte) o.push(ByteVector(Headers.Bin8) ++ size ++ bytes) - } else if (bytes.size <= Math.pow(2, 16) - 1) { + } else if (bytes.size <= (1 << 16) - 1) { val size = ByteVector.fromShort(bytes.size.toShort) o.push(ByteVector(Headers.Bin16) ++ size ++ bytes) - } else if (fitsIn(bytes.size.toInt, 32)) { + } else if (bytes.size <= (1L << 32) - 1) { val size = ByteVector.fromInt(bytes.size.toInt) /* Max length of str32 (incl. type and length info) is 2^32 + 4 bytes * which is more than Chunk can handle at once @@ -159,7 +154,7 @@ private[low] object ItemSerializer { case MsgpackItem.Array(size) => if (size <= 15) { o.push(ByteVector.fromByte((arrayMask | size).toByte)) - } else if (size <= Math.pow(2, 16) - 1) { + } else if (size <= (1L << 16) - 1) { val s = ByteVector.fromShort(size.toShort) o.push(ByteVector(Headers.Array16) ++ s) } else if (size <= (1L << 32) - 1) { @@ -172,7 +167,7 @@ private[low] object ItemSerializer { case MsgpackItem.Map(size) => if (size <= 15) { o.push(ByteVector.fromByte((mapMask | size).toByte)) - } else if (size <= Math.pow(2, 16) - 1) { + } else if (size <= (1L << 16) - 1) { val s = ByteVector.fromShort(size.toShort) o.push(ByteVector(Headers.Map16) ++ s) } else if (size <= (1L << 32) - 1) { @@ -194,10 +189,10 @@ private[low] object ItemSerializer { o.push((ByteVector(Headers.FixExt8) :+ tpe) ++ bs.padLeft(8)) } else if (bs.size <= 16) { o.push((ByteVector(Headers.FixExt16) :+ tpe) ++ bs.padLeft(16)) - } else if (bs.size <= Math.pow(2, 8) - 1) { + } else if (bs.size <= (1 << 8) - 1) { val size = ByteVector.fromByte(bs.size.toByte) o.push((ByteVector(Headers.Ext8) ++ size :+ tpe) ++ bs) - } else if (bs.size <= Math.pow(2, 16) - 1) { + } else if (bs.size <= (1 << 16) - 1) { val size = ByteVector.fromShort(bs.size.toShort) o.push((ByteVector(Headers.Ext16) ++ size :+ tpe) ++ bs) } else { diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala index c1f27043..59cf0f2a 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemValidator.scala @@ -50,13 +50,13 @@ private[low] object ItemValidator { PullNone case MsgpackItem.Str(bytes) => - if (bytes.size > Math.pow(2, 32) - 1) + if (bytes.size > (1L << 32) - 1) Pull.raiseError(MsgpackMalformedItemException("String exceeds (2^32)-1 bytes", Some(position))) else PullNone case MsgpackItem.Bin(bytes) => - if (bytes.size > Math.pow(2, 32) - 1) + if (bytes.size > (1L << 32) - 1) Pull.raiseError(MsgpackMalformedItemException("Bin exceeds (2^32)-1 bytes", Some(position))) else PullNone @@ -82,7 +82,7 @@ private[low] object ItemValidator { Pull.pure(Some(Expect(size * 2, position))) case MsgpackItem.Extension(_, bytes) => - if (bytes.size > Math.pow(2, 32) - 1) + if (bytes.size > (1L << 32) - 1) Pull.raiseError(MsgpackMalformedItemException("Extension data exceeds (2^32)-1 bytes", Some(position))) else PullNone From fce10832e4444c40952266af94d71e1c6d42c1c6 Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Mon, 23 Sep 2024 19:49:08 +0200 Subject: [PATCH 27/33] Use `.redeem` in msgpack validation spec --- .../src/test/scala/fs2/data/msgpack/ValidationSpec.scala | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala index 5edd065a..a9c5f650 100644 --- a/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala +++ b/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala @@ -35,8 +35,7 @@ object ValidationSpec extends SimpleIOSuite { .through(low.toBinary[F]) .compile .drain - .map(_ => failure(s"Expected error for item ${lhs}")) - .handleError(expect.same(_, rhs)) + .redeem(expect.same(_, rhs), _ => failure(s"Expected error for item ${lhs}")) } .compile .foldMonoid @@ -50,8 +49,7 @@ object ValidationSpec extends SimpleIOSuite { .through(low.toBinary[F]) .compile .drain - .map(_ => failure(s"Expected error for item ${lhs}")) - .handleError(expect.same(_, rhs)) + .redeem(expect.same(_, rhs), _ => failure(s"Expected error for item ${lhs}")) } .compile .foldMonoid From 989ec8aeda017eff4393428477cad6dbcf7df1e0 Mon Sep 17 00:00:00 2001 From: Mariusz Jakoniuk Date: Sun, 27 Oct 2024 21:47:13 +0100 Subject: [PATCH 28/33] Use binary data in msgpack serializer benchmark instead of relying on conversion from hex representation via scodec. This also makes it easier to load input data into a properly chunked stream. --- .../src/main/resources/twitter_msgpack.mp | Bin 0 -> 401510 bytes .../src/main/resources/twitter_msgpack.txt | 1 - .../MsgPackItemSerializerBenchmarks.scala | 26 ++++++------------ 3 files changed, 8 insertions(+), 19 deletions(-) create mode 100644 benchmarks/src/main/resources/twitter_msgpack.mp delete mode 100644 benchmarks/src/main/resources/twitter_msgpack.txt diff --git a/benchmarks/src/main/resources/twitter_msgpack.mp b/benchmarks/src/main/resources/twitter_msgpack.mp new file mode 100644 index 0000000000000000000000000000000000000000..c2219ce35223f76860248f7d6e8dd1f3717f7f79 GIT binary patch literal 401510 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 P=pMF>MPHH6) Date: Wed, 30 Oct 2024 22:31:18 +0100 Subject: [PATCH 29/33] Use binary data in msgpack parser benchmark Fixes the benchmark and reflects changes made in 989ec8a. --- .../MsgPackItemParserBenchmarks.scala | 25 ++++++++----------- .../MsgPackItemSerializerBenchmarks.scala | 3 --- .../fs2/data/msgpack/ValidationSpec.scala | 1 - 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemParserBenchmarks.scala b/benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemParserBenchmarks.scala index a088cfe6..3eb39e6a 100644 --- a/benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemParserBenchmarks.scala +++ b/benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemParserBenchmarks.scala @@ -22,7 +22,6 @@ import org.openjdk.jmh.annotations._ import cats.effect.SyncIO -import scodec.bits._ import fs2._ @OutputTimeUnit(TimeUnit.MICROSECONDS) @@ -32,23 +31,19 @@ import fs2._ @Warmup(iterations = 3, time = 2) @Measurement(iterations = 10, time = 2) class MsgPackItemParserBenchmarks { - - // The file contains hex representation of the values so we have to convert it - val msgpackBytes: ByteVector = ByteVector - .fromHex( - fs2.io - .readClassLoaderResource[SyncIO]("twitter_msgpack.txt", 4096) - .through(fs2.text.utf8.decode) - .compile - .string - .unsafeRunSync() - ) - .get + val msgpackBytes: Stream[SyncIO, Byte] = + fs2.io + .readClassLoaderResource[SyncIO]("twitter_msgpack.mp", 4096) + .chunks + .compile + .toList + .unsafeRunSync() + .map(Stream.chunk) + .fold(Stream.empty)(_ ++ _) @Benchmark def parseMsgpackItems() = - Stream - .chunk(Chunk.byteVector(msgpackBytes)) + msgpackBytes .through(fs2.data.msgpack.low.items[SyncIO]) .compile .drain diff --git a/benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemSerializerBenchmarks.scala b/benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemSerializerBenchmarks.scala index ce9244c1..c1374635 100644 --- a/benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemSerializerBenchmarks.scala +++ b/benchmarks/src/main/scala/fs2/data/benchmarks/MsgPackItemSerializerBenchmarks.scala @@ -22,9 +22,6 @@ import org.openjdk.jmh.annotations._ import cats.effect.SyncIO -import scodec.bits._ -import fs2._ - @OutputTimeUnit(TimeUnit.MICROSECONDS) @BenchmarkMode(Array(Mode.AverageTime)) @State(org.openjdk.jmh.annotations.Scope.Benchmark) diff --git a/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala index a9c5f650..a804766f 100644 --- a/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala +++ b/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala @@ -20,7 +20,6 @@ package msgpack import cats.effect._ import low.MsgpackItem -import scodec.bits.ByteVector import weaver._ import scodec.bits._ import cats.implicits._ From dad4569e4a6635ac8c0673c4f7317480856cbba1 Mon Sep 17 00:00:00 2001 From: Yannick Heiber Date: Mon, 27 Jan 2025 13:13:03 +0100 Subject: [PATCH 30/33] Make Out wrapper class an AnyVal --- .../fs2/data/msgpack/low/internal/ItemSerializer.scala | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala index 8cd09b8b..ad67e04f 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala @@ -39,8 +39,8 @@ private[low] object ItemSerializer { * * @param contents buffered [[Chunk]] */ - private class Out[F[_]](contents: Chunk[Byte]) { - private val limit = 4096 + private class Out[F[_]](val contents: Chunk[Byte]) extends AnyVal { + import Out.limit /** Pushes `bv` into the buffer and emits the buffer if it reaches the limit. */ @@ -74,7 +74,11 @@ private[low] object ItemSerializer { /** Outputs the whole buffer. */ @inline - def flush = Pull.output(contents) + def flush: Pull[Nothing, Byte, Unit] = Pull.output(contents) + } + + private object Out { + @inline private final val limit: Int = 4096 } @inline From 28da5372c250a3400a4fe6c2cc51e5a449216bb5 Mon Sep 17 00:00:00 2001 From: Yannick Heiber Date: Mon, 27 Jan 2025 13:15:54 +0100 Subject: [PATCH 31/33] Use expect to simplify test case --- msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala index 79a889b5..5b59d743 100644 --- a/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala +++ b/msgpack/src/test/scala/fs2/data/msgpack/SerializerSpec.scala @@ -178,10 +178,7 @@ object SerializerSpec extends SimpleIOSuite { pre <- round(data) processed <- round(pre) } yield { - if (processed == pre) - success - else - failure(s"Serializer should be fixpoint for ${pre} but it emitted ${processed}") + expect(pre === processed, s"Serializer should be fixpoint for $pre but it emitted $processed") } out.compile.foldMonoid From f5573322aa944e262145993d29d2351a02a657ad Mon Sep 17 00:00:00 2001 From: Yannick Heiber Date: Mon, 27 Jan 2025 13:17:31 +0100 Subject: [PATCH 32/33] Make overloaded test helpers DRY through delegation --- .../scala/fs2/data/msgpack/ValidationSpec.scala | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala b/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala index a804766f..04979d24 100644 --- a/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala +++ b/msgpack/src/test/scala/fs2/data/msgpack/ValidationSpec.scala @@ -26,18 +26,7 @@ import cats.implicits._ object ValidationSpec extends SimpleIOSuite { def validation1[F[_]: Sync](cases: (MsgpackItem, Throwable)*): F[Expectations] = - Stream - .emits(cases) - .evalMap { case (lhs, rhs) => - Stream - .emit(lhs) - .through(low.toBinary[F]) - .compile - .drain - .redeem(expect.same(_, rhs), _ => failure(s"Expected error for item ${lhs}")) - } - .compile - .foldMonoid + validation[F](cases.map(_.leftMap(List(_)))*) def validation[F[_]: Sync](cases: (List[MsgpackItem], Throwable)*): F[Expectations] = Stream @@ -48,7 +37,7 @@ object ValidationSpec extends SimpleIOSuite { .through(low.toBinary[F]) .compile .drain - .redeem(expect.same(_, rhs), _ => failure(s"Expected error for item ${lhs}")) + .redeem(expect.same(_, rhs), _ => failure(s"Expected error for item $lhs")) } .compile .foldMonoid From 9f5c52d30748a109e961de49bf1bcf311720df5c Mon Sep 17 00:00:00 2001 From: Yannick Heiber Date: Mon, 27 Jan 2025 15:38:13 +0100 Subject: [PATCH 33/33] Make Out no longer an AnyVal --- .../scala/fs2/data/msgpack/low/internal/ItemSerializer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala index ad67e04f..6c0ddb0d 100644 --- a/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala +++ b/msgpack/src/main/scala/fs2/data/msgpack/low/internal/ItemSerializer.scala @@ -39,7 +39,7 @@ private[low] object ItemSerializer { * * @param contents buffered [[Chunk]] */ - private class Out[F[_]](val contents: Chunk[Byte]) extends AnyVal { + private class Out[F[_]](val contents: Chunk[Byte]) { // Note this can't be an effective AnyVal as it's used in a generic import Out.limit /** Pushes `bv` into the buffer and emits the buffer if it reaches the limit.