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
|
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}")
|
||||||
|
|
|
||||||
|
|
@ -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
|
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]
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue