From b71ce8fe9fb001c7b36e06b9f80aa63761276c41 Mon Sep 17 00:00:00 2001 From: Michael Pilquist Date: Thu, 4 Jul 2024 09:09:02 -0400 Subject: [PATCH] Initial draft of new hashing package --- .../fs2/hashing/HashCompanionPlatform.scala | 31 ++++++++++ .../fs2/hashing/HashCompanionPlatform.scala | 43 +++++++++++++ .../fs2/hashing/HashCompanionPlatform.scala | 31 ++++++++++ .../src/main/scala/fs2/hashing/Hash.scala | 62 +++++++++++++++++++ .../hashing/HashVerificationException.scala | 32 ++++++++++ .../src/main/scala/fs2/hashing/Hashing.scala | 52 ++++++++++++++++ 6 files changed, 251 insertions(+) create mode 100644 core/js/src/main/scala/fs2/hashing/HashCompanionPlatform.scala create mode 100644 core/jvm/src/main/scala/fs2/hashing/HashCompanionPlatform.scala create mode 100644 core/native/src/main/scala/fs2/hashing/HashCompanionPlatform.scala create mode 100644 core/shared/src/main/scala/fs2/hashing/Hash.scala create mode 100644 core/shared/src/main/scala/fs2/hashing/HashVerificationException.scala create mode 100644 core/shared/src/main/scala/fs2/hashing/Hashing.scala diff --git a/core/js/src/main/scala/fs2/hashing/HashCompanionPlatform.scala b/core/js/src/main/scala/fs2/hashing/HashCompanionPlatform.scala new file mode 100644 index 0000000000..638ac59a0d --- /dev/null +++ b/core/js/src/main/scala/fs2/hashing/HashCompanionPlatform.scala @@ -0,0 +1,31 @@ +/* + * 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 hashing + +import cats.effect.Sync + +trait HashCompanionPlatform { + + def unsafe[F[_]: Sync](algorithm: String): Hash[F] = + ??? +} diff --git a/core/jvm/src/main/scala/fs2/hashing/HashCompanionPlatform.scala b/core/jvm/src/main/scala/fs2/hashing/HashCompanionPlatform.scala new file mode 100644 index 0000000000..efad2a2445 --- /dev/null +++ b/core/jvm/src/main/scala/fs2/hashing/HashCompanionPlatform.scala @@ -0,0 +1,43 @@ +/* + * 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 hashing + +import cats.effect.Sync + +import java.security.MessageDigest + +private[hashing] trait HashCompanionPlatform { + def unsafe[F[_]: Sync](algorithm: String): Hash[F] = + unsafeFromMessageDigest(MessageDigest.getInstance(algorithm)) + + def unsafeFromMessageDigest[F[_]: Sync](d: MessageDigest): Hash[F] = + new Hash[F] { + def addChunk(bytes: Chunk[Byte]): F[Unit] = Sync[F].delay(unsafeAddChunk(bytes.toArraySlice)) + def computeAndReset: F[Chunk[Byte]] = Sync[F].delay(unsafeComputeAndReset()) + + def unsafeAddChunk(slice: Chunk.ArraySlice[Byte]): Unit = + d.update(slice.values, slice.offset, slice.size) + + def unsafeComputeAndReset(): Chunk[Byte] = Chunk.array(d.digest()) + } +} diff --git a/core/native/src/main/scala/fs2/hashing/HashCompanionPlatform.scala b/core/native/src/main/scala/fs2/hashing/HashCompanionPlatform.scala new file mode 100644 index 0000000000..638ac59a0d --- /dev/null +++ b/core/native/src/main/scala/fs2/hashing/HashCompanionPlatform.scala @@ -0,0 +1,31 @@ +/* + * 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 hashing + +import cats.effect.Sync + +trait HashCompanionPlatform { + + def unsafe[F[_]: Sync](algorithm: String): Hash[F] = + ??? +} diff --git a/core/shared/src/main/scala/fs2/hashing/Hash.scala b/core/shared/src/main/scala/fs2/hashing/Hash.scala new file mode 100644 index 0000000000..07a2efdde4 --- /dev/null +++ b/core/shared/src/main/scala/fs2/hashing/Hash.scala @@ -0,0 +1,62 @@ +/* + * 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 hashing + +import cats.effect.Sync + +trait Hash[F[_]] { + def addChunk(bytes: Chunk[Byte]): F[Unit] + def computeAndReset: F[Chunk[Byte]] + + protected def unsafeAddChunk(slice: Chunk.ArraySlice[Byte]): Unit + protected def unsafeComputeAndReset(): Chunk[Byte] + + def update: Pipe[F, Byte, Byte] = + _.mapChunks { c => + unsafeAddChunk(c.toArraySlice) + c + } + + def observe(source: Stream[F, Byte], sink: Pipe[F, Byte, Nothing]): Stream[F, Byte] = + update(source).through(sink) ++ Stream.evalUnChunk(computeAndReset) + + def hash: Pipe[F, Byte, Byte] = + source => observe(source, _.drain) + + def verify(expected: Chunk[Byte])(implicit F: RaiseThrowable[F]): Pipe[F, Byte, Byte] = + source => + update(source) + .onComplete( + Stream + .eval(computeAndReset) + .flatMap(actual => + if (actual == expected) Stream.empty + else Stream.raiseError(HashVerificationException(expected, actual)) + ) + ) +} + +object Hash extends HashCompanionPlatform { + def apply[F[_]: Sync](algorithm: String): F[Hash[F]] = + Sync[F].delay(unsafe(algorithm)) +} diff --git a/core/shared/src/main/scala/fs2/hashing/HashVerificationException.scala b/core/shared/src/main/scala/fs2/hashing/HashVerificationException.scala new file mode 100644 index 0000000000..e5210665c2 --- /dev/null +++ b/core/shared/src/main/scala/fs2/hashing/HashVerificationException.scala @@ -0,0 +1,32 @@ +/* + * 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 hashing + +import java.io.IOException + +case class HashVerificationException( + expected: Chunk[Byte], + actual: Chunk[Byte] +) extends IOException( + s"Digest did not match, expected: ${expected.toByteVector.toHex}, actual: ${actual.toByteVector.toHex}" + ) diff --git a/core/shared/src/main/scala/fs2/hashing/Hashing.scala b/core/shared/src/main/scala/fs2/hashing/Hashing.scala new file mode 100644 index 0000000000..af6f6f44f7 --- /dev/null +++ b/core/shared/src/main/scala/fs2/hashing/Hashing.scala @@ -0,0 +1,52 @@ +/* + * 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 hashing + +import cats.effect.Sync + +/** Capability trait that provides hashing. + * + * The [[create]] method returns an action that instantiates a fresh `Hash` object. + * `Hash` is a mutable object that supports incremental computation of hashes. A `Hash` + * instance should be created for each hash you want to compute. + */ +trait Hashing[F[_]] { + def create(algorithm: String): F[Hash[F]] + def md5: F[Hash[F]] = create("MD-5") + def sha1: F[Hash[F]] = create("SHA-1") + def sha256: F[Hash[F]] = create("SHA-256") + def sha384: F[Hash[F]] = create("SHA-384") + def sha512: F[Hash[F]] = create("SHA-512") + + def hashWith(hash: F[Hash[F]]): Pipe[F, Byte, Byte] = + source => Stream.eval(hash).flatMap(h => h.hash(source)) +} + +object Hashing { + implicit def apply[F[_]](implicit F: Hashing[F]): F.type = F + + implicit def forSync[F[_]: Sync]: Hashing[F] = new Hashing[F] { + def create(algorithm: String): F[Hash[F]] = + Hash[F](algorithm) + } +}