WIP, beginning work on hash implementation.

This commit is contained in:
Pat Garrity 2023-11-28 21:47:16 -06:00
parent ca2a85c81b
commit a430bd97d1
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
7 changed files with 169 additions and 7 deletions

View file

@ -77,12 +77,11 @@ but ShortForm imposes a configurable limit (10kb by default).
CREATE TABLE assets( CREATE TABLE assets(
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
external_id UUID NOT NULL UNIQUE, external_id UUID NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL,
created_by BIGINT NOT NULL,
mime TEXT NOT NULL,
title TEXT NOT NULL, title TEXT NOT NULL,
extension TEXT NOT NULL, extension TEXT NOT NULL,
hash TEXT NOT NULL hash TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
created_by BIGINT NOT NULL
); );
``` ```

View file

@ -0,0 +1,45 @@
package gs.shortform.crypto
import java.security.MessageDigest
import java.io.BufferedInputStream
object Sha256:
/**
* Calculate the SHA-256 hash for some data.
*
* @param data The data to hash.
* @return The base64-encoded [[Hash]] value.
*/
def calculateHash(data: Array[Byte]): Hash =
val digest: MessageDigest = MessageDigest.getInstance(Sha256.Algorithm)
Hash.encode(digest.digest(data))
/**
* Consume the entire given stream and calculate its SHA-256 hash. This
* function _always_ closes the underlying stream.
*
* @param data The data to hash.
* @return The base64-encoded [[Hash]] value.
*/
def consumeToHash(data: BufferedInputStream): Hash =
try
// TODO: Need sum catz?
val digest: MessageDigest = MessageDigest.getInstance(Sha256.Algorithm)
val buffer: Array[Byte] = Array.ofDim[Byte](8192)
var count: Int = data.read(buffer)
while
count > 0
do
digest.update(buffer, 0, count)
count = data.read(buffer)
Hash.encode(digest.digest())
finally
data.close()
/**
* JCA Algorithm name for SHA-256.
*/
val Algorithm: String = "SHA-256"
end Sha256

View file

@ -0,0 +1,33 @@
package gs.shortform.model
import gs.uuid.UUID
import gs.shortform.crypto.Hash
case class Asset(
externalId: UUID,
title: Title,
extension: Asset.Extension,
hash: Hash,
createdAt: CreatedAt,
createdBy: Username
)
object Asset:
/**
* Represents a file extension for some [[Asset]].
*/
opaque type Extension = String
object Extension:
def apply(value: String): Extension = value
given CanEqual[Extension, Extension] = CanEqual.derived
extension (ext: Extension)
def str(): String = ext
end Extension
end Asset

View file

@ -0,0 +1,25 @@
package gs.shortform.model
import gs.shortform.error.ShortFormError
sealed trait ModelError extends ShortFormError
object ModelError:
/**
* Results from a [[Tag]] failing validation for being too short.
*
* @param candidate The candidate string.
* @param minimumLength The minimum required length.
*/
case class TagTooShort(candidate: String, minimumLength: Int) extends ModelError
/**
* Results from a [[Tag]] failing validation for being too long.
*
* @param candidate The candidate string.
* @param maximumLength The maximum required length.
*/
case class TagTooLong(candidate: String, maximumLength: Int) extends ModelError
end ModelError

View file

@ -15,11 +15,13 @@ import gs.shortform.crypto.Hash
* @param createdBy User who created this post. * @param createdBy User who created this post.
* @param title Display title for this post. * @param title Display title for this post.
* @param hash Hash of the primary rendered file for this post. * @param hash Hash of the primary rendered file for this post.
* @param tags List of [[Tag]] applied to this post.
*/ */
case class Post( case class Post(
externalId: UUID, externalId: UUID,
createdAt: CreatedAt, createdAt: CreatedAt,
createdBy: Username, createdBy: Username,
title: Title, title: Title,
hash: Hash hash: Hash,
tags: List[Tag]
) )

View file

@ -0,0 +1,57 @@
package gs.shortform.model
import gs.shortform.error.ShortFormError
/**
* Arbitrary short string which can be used to annotate [[Post]]s.
*/
opaque type Tag = String
object Tag:
object Constraints:
/**
* The minimum allowed length for tags.
*/
val MinimumLength: Int = 1
/**
* The maximum allowed length for tags.
*/
val MaximumLength: Int = 32
end Constraints
/**
* Instantiate a new [[Tag]]. This function is unsafe.
*
* @param value The tag value.
* @return The new tag.
*/
def apply(value: String): Tag = value
/**
* Validate some candidate value to instantiate a new [[Tag]].
*
* @param value The candidate string value.
* @return The new tag, or an error if the input was invalid.
*/
def validate(value: String): Either[ShortFormError, Tag] =
if value.length() < Constraints.MinimumLength then
Left(ModelError.TagTooShort(value, Constraints.MinimumLength))
else if value.length() > Constraints.MaximumLength then
Left(ModelError.TagTooLong(value, Constraints.MaximumLength))
else
Right(value)
given CanEqual[Tag, Tag] = CanEqual.derived
extension (title: Tag)
/**
* Render this [[Tag]] as a string.
*/
def str(): String = title
end Tag

View file

@ -12,8 +12,9 @@ object Username:
given CanEqual[Username, Username] = CanEqual.derived given CanEqual[Username, Username] = CanEqual.derived
extension (title: Username) extension (title: Username)
/**
* Render this [[Username]] as a string.
*/
def str(): String = title def str(): String = title
end Username end Username