Working on some more helper things.
Some checks failed
/ Build and Release Library (push) Has been cancelled
Some checks failed
/ Build and Release Library (push) Has been cancelled
This commit is contained in:
parent
479a9f6753
commit
3e7de767c7
5 changed files with 141 additions and 0 deletions
|
|
@ -72,6 +72,8 @@ lazy val io = project
|
|||
|
||||
lazy val effect = project
|
||||
.in(file("modules/effect"))
|
||||
.dependsOn(core)
|
||||
.dependsOn(io % "test->compile")
|
||||
.settings(sharedSettings)
|
||||
.settings(testSettings)
|
||||
.settings(name := s"${gsProjectName.value}-effect-v${semVerMajor.value}")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
BIN
modules/effect/src/test/resources/sha256-input
Normal file
BIN
modules/effect/src/test/resources/sha256-input
Normal file
Binary file not shown.
|
|
@ -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
|
||||
|
|
@ -5,6 +5,8 @@ import java.nio.charset.StandardCharsets
|
|||
import java.security.MessageDigest
|
||||
|
||||
/** Opaque type representing a SHA-256 hash.
|
||||
*
|
||||
* The actual underlying type is an array of bytes.
|
||||
*/
|
||||
opaque type SHA256 = Array[Byte]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue