diff --git a/protocols/shared/src/main/scala-2/fs2/protocols/ntp/NTPPacketCompat.scala b/protocols/shared/src/main/scala-2/fs2/protocols/ntp/NTPPacketCompat.scala new file mode 100644 index 0000000000..6463e11605 --- /dev/null +++ b/protocols/shared/src/main/scala-2/fs2/protocols/ntp/NTPPacketCompat.scala @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2.protocols.ntp + +import org.typelevel.scalaccompat.annotation.* +import scodec.* +import shapeless.Generic +import shapeless.Nat +@nowarn3("msg=unused import") +private[ntp] trait NTPPacketCompat { self: NTPPacket.type => + import scodec.compat.* + def codec: Codec[NTPPacket] = layout + .xmap( + { case liVnMode *: rest => + Generic[NTPPacket].from(liVnMode ++ rest) + }, + Generic[NTPPacket].to(_).split[Nat._3] match { + case (liVnMode, rest) => liVnMode *: rest + } + ) +} diff --git a/protocols/shared/src/main/scala-3/fs2/protocols/ntp/NTPPacketCompat.scala b/protocols/shared/src/main/scala-3/fs2/protocols/ntp/NTPPacketCompat.scala new file mode 100644 index 0000000000..1707199445 --- /dev/null +++ b/protocols/shared/src/main/scala-3/fs2/protocols/ntp/NTPPacketCompat.scala @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2.protocols.ntp + +import scodec.* +import scala.deriving.Mirror + +private[ntp] trait NTPPacketCompat { self: NTPPacket.type => + def codec: Codec[NTPPacket] = layout + .xmap( + { case liVnMode *: rest => + summon[Mirror.ProductOf[NTPPacket]].fromProduct(liVnMode ++ rest) + }, + Tuple.fromProductTyped(_).splitAt(3) match { + case (liVnMode, rest) => liVnMode *: rest + } + ) +} diff --git a/protocols/shared/src/main/scala/fs2/protocols/ntp/LeapIndicator.scala b/protocols/shared/src/main/scala/fs2/protocols/ntp/LeapIndicator.scala new file mode 100644 index 0000000000..9c787a61db --- /dev/null +++ b/protocols/shared/src/main/scala/fs2/protocols/ntp/LeapIndicator.scala @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2.protocols.ntp + +sealed trait LeapIndicator { + def value: Int = this match { + case LeapIndicator.NoAdjustment => 0 + case LeapIndicator.AddSecond => 1 + case LeapIndicator.SubtractSecond => 2 + case LeapIndicator.Unsynchronized => 3 + } +} + +object LeapIndicator { + def from(value: Int): LeapIndicator = value match { + case 0 => LeapIndicator.NoAdjustment + case 1 => LeapIndicator.AddSecond + case 2 => LeapIndicator.SubtractSecond + case 3 => LeapIndicator.Unsynchronized + } + case object NoAdjustment extends LeapIndicator + case object AddSecond extends LeapIndicator + case object SubtractSecond extends LeapIndicator + case object Unsynchronized extends LeapIndicator +} diff --git a/protocols/shared/src/main/scala/fs2/protocols/ntp/NTPMode.scala b/protocols/shared/src/main/scala/fs2/protocols/ntp/NTPMode.scala new file mode 100644 index 0000000000..1111da7293 --- /dev/null +++ b/protocols/shared/src/main/scala/fs2/protocols/ntp/NTPMode.scala @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2.protocols.ntp + +sealed trait NTPMode { + def value: Int = this match { + case NTPMode.Reserved => 0 + case NTPMode.SymmetricActive => 1 + case NTPMode.SymmetricPassive => 2 + case NTPMode.Client => 3 + case NTPMode.Server => 4 + case NTPMode.Broadcast => 5 + case NTPMode.ControlMessage => 6 + case NTPMode.ReservedPrivate => 7 + } +} +object NTPMode { + case object Reserved extends NTPMode + case object SymmetricActive extends NTPMode + case object SymmetricPassive extends NTPMode + case object Client extends NTPMode + case object Server extends NTPMode + + case object Broadcast extends NTPMode + case object ControlMessage extends NTPMode + case object ReservedPrivate extends NTPMode + + def from(mode: Int): NTPMode = mode match { + case 0 => Reserved + case 1 => SymmetricActive + case 2 => SymmetricPassive + case 3 => Client + case 4 => Server + case 5 => Broadcast + case 6 => ControlMessage + case 7 => ReservedPrivate + } +} diff --git a/protocols/shared/src/main/scala/fs2/protocols/ntp/NTPPacket.scala b/protocols/shared/src/main/scala/fs2/protocols/ntp/NTPPacket.scala new file mode 100644 index 0000000000..78c821bba3 --- /dev/null +++ b/protocols/shared/src/main/scala/fs2/protocols/ntp/NTPPacket.scala @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2.protocols.ntp + +import scodec.* +import scodec.bits.* +import scodec.codecs.* +import org.typelevel.scalaccompat.annotation.* + +case class NTPPacket( + li: LeapIndicator, + version: Int, + mode: NTPMode, + stratum: Int, + poll: Int, + precision: Int, + rootDelay: Long, + rootDispersion: Long, + referenceID: Long, + referenceTime: BigInt, + originTime: BigInt, + receiveTime: BigInt, + transmitTime: BigInt +) +@nowarn3("msg=unused import") +object NTPPacket extends NTPPacketCompat { + import scodec.compat.* + + val default = NTPPacket( + li = LeapIndicator.NoAdjustment, + version = 3, + mode = NTPMode.Client, + stratum = 0, + poll = 0, + precision = 0x20, + rootDelay = 0, + rootDispersion = 0, + referenceID = 0, + referenceTime = 0, + originTime = 0, + receiveTime = 0, + transmitTime = 0 + ) + + private[ntp] val ulong64: Codec[BigInt] = bytes(8) + .xmap( + _.toBigInt(false), + ByteVector.fromBigInt(_, Some(8)) + ) + + private[ntp] val liVnMode: Codec[LeapIndicator *: Int *: NTPMode *: EmptyTuple] = uint8.xmap( + octet => { + val li = LeapIndicator.from(octet >> 6) + val version = (octet >> 3) & 0x07 + val mode = NTPMode.from(octet & 0x07) + li *: version *: mode *: EmptyTuple + }, + { case li *: version *: mode *: EmptyTuple => + ((li.value & 0x3) << 6) | ((version & 0x07) << 3) | (mode.value & 0x07) + } + ) + + private[ntp] def layout = + ("li / version / mode" | liVnMode) :: + ("stratum" | uint8) :: + ("poll" | uint8) :: + ("precision" | uint8) :: + ("root delay" | uint32) :: + ("root dispersion" | uint32) :: + ("reference id" | uint32) :: + ("reference time" | ulong64) :: + ("origin time" | ulong64) :: + ("receive time" | ulong64) :: + ("transmit time" | ulong64) +} diff --git a/protocols/shared/src/test/scala/fs2/protocols/ntp/NTPPacketTest.scala b/protocols/shared/src/test/scala/fs2/protocols/ntp/NTPPacketTest.scala new file mode 100644 index 0000000000..31a1f0c2fc --- /dev/null +++ b/protocols/shared/src/test/scala/fs2/protocols/ntp/NTPPacketTest.scala @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2013 Functional Streams for Scala + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fs2 +package protocols +package ntp +import org.scalacheck.Arbitrary +import org.scalacheck.Gen +import org.scalacheck.Prop +import scodec.* +import scodec.Attempt.Successful +import scodec.bits.* +import munit.Location +import org.typelevel.scalaccompat.annotation.* + +@nowarn3("msg=unused import") +class NTPPacketTest extends Fs2Suite { + import scodec.compat.* + import NTPPacketTestData._ + test("ntp packet default roundtrip") { + roundtrip[NTPPacket](NTPPacket.codec, NTPPacket.default) + } + + test("ulong64 roundtrip without long overflow") { + val Successful(encode) = NTPPacket.ulong64.encode(BigInt(Long.MaxValue) + 1): @unchecked + val Successful(decode) = NTPPacket.ulong64.decode(encode): @unchecked + assertEquals(decode.value, BigInt(Long.MaxValue) + 1) + } + + test("arbitrary li vi mode roundtrip") { + Prop.forAll((tuple: LeapIndicator *: Int *: NTPMode *: EmptyTuple) => + roundtrip(NTPPacket.liVnMode, tuple) + ) + } + + test("arbitrary ntp packet roundtrip") { + Prop.forAll((packet: NTPPacket) => roundtrip(NTPPacket.codec, packet)) + } + + private def roundtrip[A](codec: Codec[A], value: A)(implicit loc: Location): Unit = { + val Successful(encode) = codec.encode(value): @unchecked + val Successful(decode) = codec.decode(encode): @unchecked + assertEquals(decode.value, value) + } +} + +@nowarn3("msg=unused import") +object NTPPacketTestData { + import scodec.compat.* + lazy val genUInt8 = Gen.choose(0, 0xff) + lazy val genUInt32 = Gen.choose(0L, 0xffffffffL) + lazy val genULong64 = Gen.choose(BigInt(0), BigInt("18446744073709551615")) + lazy val genLeapIndicator = Gen.choose[Int](0, 3).map(LeapIndicator.from) + lazy val genNTPMode = Gen.choose[Int](0, 7).map(NTPMode.from) + lazy val genLiVnMode = for { + li <- genLeapIndicator + vn <- Gen.choose[Int](bin"001".toInt(false), bin"111".toInt(false)) + mode <- genNTPMode + } yield li *: vn *: mode *: EmptyTuple + lazy val genNTPPacket = for { + liVnMode <- genLiVnMode + li *: vn *: mode *: EmptyTuple = liVnMode + stratum <- genUInt8 + poll <- genUInt8 + precision <- genUInt8 + rootDelay <- genUInt32 + rootDispersion <- genUInt32 + referenceID <- genUInt32 + referenceTime <- genULong64 + originTime <- genULong64 + receiveTime <- genULong64 + transmitTime <- genULong64 + } yield NTPPacket( + li, + vn, + mode, + stratum, + poll, + precision, + rootDelay, + rootDispersion, + referenceID, + referenceTime, + originTime, + receiveTime, + transmitTime + ) + implicit val arbitraryLiVnMode: Arbitrary[LeapIndicator *: Int *: NTPMode *: EmptyTuple] = + Arbitrary(genLiVnMode) + implicit val arbitraryNTPPacket: Arbitrary[NTPPacket] = Arbitrary(genNTPPacket) +}