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(
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
external_id UUID NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL,
created_by BIGINT NOT NULL,
mime TEXT NOT NULL,
title 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 title Display title 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(
externalId: UUID,
createdAt: CreatedAt,
createdBy: Username,
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
extension (title: Username)
/**
* Render this [[Username]] as a string.
*/
def str(): String = title
end Username