Working on some more helper things.
Some checks failed
/ Build and Release Library (push) Has been cancelled

This commit is contained in:
Pat Garrity 2026-05-03 21:20:04 -05:00
parent 479a9f6753
commit 3e7de767c7
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
5 changed files with 141 additions and 0 deletions

View file

@ -72,6 +72,8 @@ lazy val io = project
lazy val effect = project lazy val effect = project
.in(file("modules/effect")) .in(file("modules/effect"))
.dependsOn(core)
.dependsOn(io % "test->compile")
.settings(sharedSettings) .settings(sharedSettings)
.settings(testSettings) .settings(testSettings)
.settings(name := s"${gsProjectName.value}-effect-v${semVerMajor.value}") .settings(name := s"${gsProjectName.value}-effect-v${semVerMajor.value}")

View file

@ -0,0 +1,46 @@
package gs.std.v0.effect.pipes
import cats.Applicative
import fs2.Chunk
import fs2.Pipe
import fs2.Pull
import fs2.Stream
import gs.std.v0.core.SHA256
import java.security.MessageDigest
class SHA256Pipe[F[_]: Applicative] extends Pipe[F, Byte, SHA256]:
private def newDigest(): MessageDigest =
MessageDigest.getInstance(SHA256.Algorithm)
private def append(
digest: MessageDigest,
data: Chunk[Byte]
): F[Unit] =
Applicative[F].pure(digest.update(data.toArray))
def pull(
s: Stream[F, Byte],
digest: MessageDigest
): Pull[F, SHA256, Unit] =
s.pull.uncons.flatMap {
case Some((head, tail)) =>
// There is another chunk of data.
// Update the digest and then handle the next chunk.
Pull.eval(append(digest, head)) >> pull(tail, digest)
case None =>
// The stream is empty. Emit a single chunk that contains the calculated
// hash.
Pull.output(Chunk(SHA256.fromBytes(digest.digest()))) >> Pull.done
}
override def apply(v1: Stream[F, Byte]): Stream[F, SHA256] =
pull(v1, newDigest()).stream
end SHA256Pipe
object SHA256Pipe:
def apply[F[_]: Applicative] = new SHA256Pipe[F]
end SHA256Pipe

Binary file not shown.

View file

@ -0,0 +1,91 @@
package gs.std.v0.effect.pipes
import cats.effect.IO
import cats.effect.unsafe.IORuntime
import fs2.Stream
import gs.std.v0.core.SHA256
import java.security.MessageDigest
import munit.*
class SHA256PipeTests extends FunSuite:
import SHA256PipeTests.Data
given runtime: IORuntime = IORuntime.global
def io(
name: String
)(
body: => IO[Unit]
)(
using
loc: Location
): Unit =
test(name)(body.unsafeRunSync())(
using loc
)
def getDigest(): MessageDigest = MessageDigest.getInstance(SHA256.Algorithm)
def shaDefault(data: String): SHA256 =
SHA256.calculate(data)
def shaStream(data: String): IO[SHA256] =
Stream
.evals(IO(data.getBytes().toList))
.through(SHA256Pipe.apply[IO])
.compile
.toList
.map {
case sha :: Nil => sha
case Nil => fail("Received an empty SHA-256.")
case _ => fail("Received multiple SHA-256.")
}
def readResource(resourceName: String): Stream[IO, Byte] =
fs2.io.readInputStream(
fis = IO(getClass().getClassLoader().getResourceAsStream(resourceName)),
chunkSize = 1024,
closeAfterUse = true
)
io("should calculate the SHA-256 for some fixed (pre-calculated) value") {
val data = "some-fixed-value"
val expectedHex =
"d5dae4f21bc27a568842bc9b467e69bd823d54d1c9e89d5ea662493068d5100d"
val basic = shaDefault(data)
shaStream(data).map { streamed =>
assertEquals(basic.hex().data, expectedHex)
assertEquals(streamed.hex().data, expectedHex)
assert(streamed.isSame(basic))
}
}
io("should calculate the SHA-256 for a 1M file") {
readResource(Data.File1Path)
.through(SHA256Pipe[IO])
.compile
.toList
.map {
case sha :: Nil => sha
case Nil => fail("Received an empty SHA-256.")
case _ => fail("Received multiple SHA-256.")
}
.map(sha => assertEquals(sha.hex().data, Data.File1Sha))
}
end SHA256PipeTests
object SHA256PipeTests:
object Data:
val File1Path: String = "sha256-input"
val File1Sha: String =
"42d96cff2300306954c166ab75862d4e5fd7ce977bb34f7e8ee785f62244d7a0"
end Data
end SHA256PipeTests

View file

@ -5,6 +5,8 @@ import java.nio.charset.StandardCharsets
import java.security.MessageDigest import java.security.MessageDigest
/** Opaque type representing a SHA-256 hash. /** Opaque type representing a SHA-256 hash.
*
* The actual underlying type is an array of bytes.
*/ */
opaque type SHA256 = Array[Byte] opaque type SHA256 = Array[Byte]