diff --git a/modules/core/src/main/scala/src/gs/std/v0/core/ByteCount.scala b/modules/core/src/main/scala/src/gs/std/v0/core/ByteCount.scala index c59033d..ecf0219 100644 --- a/modules/core/src/main/scala/src/gs/std/v0/core/ByteCount.scala +++ b/modules/core/src/main/scala/src/gs/std/v0/core/ByteCount.scala @@ -24,6 +24,19 @@ object ByteCount: */ final val OneGigabyte: ByteCount = 1000000000 + /** Provide integer representations for common sizes in terms of number of + * bytes. These values are useful for things like array sizing that depend on + * the `Int` type. + */ + object IntSizes: + + final val Zero: Int = 0 + final val OneKilobyte: Int = 1000 + final val OneMegabyte: Int = 1000000 + final val OneGigabyte: Int = 1000000000 + + end IntSizes + /** Express the given number as a byte count. All values are normalized to the * absolute value -- negative values are coerced to positive. * diff --git a/modules/effect/src/main/scala/src/gs/std/v0/effect/Rng.scala b/modules/effect/src/main/scala/src/gs/std/v0/effect/Rng.scala index 813fce8..61a565a 100644 --- a/modules/effect/src/main/scala/src/gs/std/v0/effect/Rng.scala +++ b/modules/effect/src/main/scala/src/gs/std/v0/effect/Rng.scala @@ -3,7 +3,10 @@ package gs.std.v0.effect import cats.Applicative import cats.effect.Sync import cats.syntax.all.* +import fs2.Chunk import fs2.Stream +import gs.std.v0.core.Blob +import gs.std.v0.core.ByteCount import java.security.SecureRandom import java.util.Random @@ -154,6 +157,29 @@ trait Rng[F[_]]: bound: Double ): Stream[F, Double] + /** Produce an array of random bytes. + * + * @param count + * The number of bytes to produce. + * @return + * The array of randomly generated bytes. + */ + def nextByteArray(count: Int): F[Array[Byte]] + + /** @return + * Infinite stream of random bytes. + */ + def nextBytes(): Stream[F, Byte] + + /** Produce a blob of random bytes. + * + * @param size + * The number of bytes to produce. + * @return + * The blob of randomly generated bytes. + */ + def nextBlob(size: Int): F[Blob] + object Rng: /** Instantiate [[Rng]] using a new `java.util.Random` instance. @@ -212,6 +238,42 @@ object Rng: */ final class JavaRandom[F[_]: Sync](random: Random) extends Rng[F]: + /** @inheritDocs + */ + override def nextBytes(): Stream[F, Byte] = + Stream + .evalUnChunk[F, Byte](Sync[F].delay { + // Allocate space for the random data. Generate 1kb at once. + val bytes = new Array[Byte](ByteCount.IntSizes.OneKilobyte) + val _ = random.nextBytes(bytes) + // This chunk is _backed by_ the array -- the data isn't copied, so we + // allocate a new array each time and cede it to the chunk. + Chunk.array(bytes) + }) + .repeat + + /** @inheritDocs + */ + override def nextByteArray(count: Int): F[Array[Byte]] = + if count < 0 then + Sync[F].raiseError( + new IllegalArgumentException( + "Cannot generate a negative number of bytes." + ) + ) + else if count == 0 then Sync[F].pure(Array.empty) + else + Sync[F].delay { + val bytes = new Array[Byte](count) + val _ = random.nextBytes(bytes) + bytes + } + + /** @inheritDocs + */ + override def nextBlob(size: Int): F[Blob] = + nextByteArray(size).map(new Blob(_)) + /** @inheritDocs */ override def updateSeed(seed: Long): F[Rng[F]] = @@ -306,6 +368,21 @@ object Rng: */ final class Zero[F[_]: Applicative] extends Rng[F]: + /** @inheritDocs + */ + override def nextByteArray(count: Int): F[Array[Byte]] = + Applicative[F].pure(Array.fill[Byte](count)(0)) + + /** @inheritDocs + */ + override def nextBytes(): Stream[F, Byte] = + Stream[F, Byte](0).repeat + + /** @inheritDocs + */ + override def nextBlob(size: Int): F[Blob] = + nextByteArray(size).map(new Blob(_)) + /** @inheritDocs */ override def updateSeed(seed: Long): F[Rng[F]] = Applicative[F].pure(this)