WIP, beginning work on hash implementation.
This commit is contained in:
parent
ca2a85c81b
commit
a430bd97d1
7 changed files with 169 additions and 7 deletions
|
@ -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
|
||||
);
|
||||
```
|
||||
|
||||
|
|
|
@ -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
|
33
modules/model/src/main/scala/gs/shortform/model/Asset.scala
Normal file
33
modules/model/src/main/scala/gs/shortform/model/Asset.scala
Normal 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
|
|
@ -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
|
|
@ -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]
|
||||
)
|
||||
|
|
57
modules/model/src/main/scala/gs/shortform/model/Tag.scala
Normal file
57
modules/model/src/main/scala/gs/shortform/model/Tag.scala
Normal 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
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue