use arrayvec::ArrayString;
use core::fmt;
#[cfg(feature = "serde")]
use etwin_serde_tools::{Deserialize, Serialize};
#[cfg(feature = "sqlx")]
use sqlx::{database, postgres, Database, Postgres};
use std::str::FromStr;
use thiserror::Error;
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(try_from = "&str", into = "ArrayString<40>")
)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DigestSha1([u8; 20]);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Error)]
#[error("invalid sha1 digest size, expected = 20, actual = {0}")]
pub struct DigestSha1FromBytesError(pub usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Error)]
pub enum ParseDigestSha1Error {
#[error("wrong length, expected 40 hex characters as input, actual = {0}")]
Length(usize),
#[error("input is not a lowercase hex string: found {character:?} at offset {offset}")]
InvalidCharacter { character: char, offset: usize },
}
impl DigestSha1 {
pub fn as_slice(&self) -> &[u8] {
&self.0
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, DigestSha1FromBytesError> {
match <[u8; 20]>::try_from(bytes) {
Ok(a) => Ok(Self(a)),
Err(_) => Err(DigestSha1FromBytesError(bytes.len())),
}
}
pub fn from_hex(input: &str) -> Result<Self, ParseDigestSha1Error> {
let mut bytes = [0u8; 20];
match hex::decode_to_slice(input, &mut bytes) {
Ok(()) => Ok(Self(bytes)),
Err(hex::FromHexError::InvalidHexCharacter { c, index }) => Err(ParseDigestSha1Error::InvalidCharacter {
character: c,
offset: index,
}),
Err(hex::FromHexError::OddLength | hex::FromHexError::InvalidStringLength) => {
Err(ParseDigestSha1Error::Length(input.len()))
}
}
}
pub fn hex(&self) -> ArrayString<40> {
let mut out = [0u8; 40];
hex::encode_to_slice(self.0, &mut out).expect("encoding to hex always succeeds");
ArrayString::from_byte_string(&out).expect("converting the hex byte string to an array string always succeeds")
}
}
#[cfg(feature = "sqlx")]
impl sqlx::Type<Postgres> for DigestSha1 {
fn type_info() -> postgres::PgTypeInfo {
postgres::PgTypeInfo::with_name("digest_sha1")
}
fn compatible(ty: &postgres::PgTypeInfo) -> bool {
*ty == Self::type_info() || <&[u8] as sqlx::Type<Postgres>>::compatible(ty)
}
}
#[cfg(feature = "sqlx")]
impl<'r, Db: Database> sqlx::Decode<'r, Db> for DigestSha1
where
&'r [u8]: sqlx::Decode<'r, Db>,
{
fn decode(
value: <Db as database::HasValueRef<'r>>::ValueRef,
) -> Result<DigestSha1, Box<dyn std::error::Error + 'static + Send + Sync>> {
let value: &[u8] = <&[u8] as sqlx::Decode<Db>>::decode(value)?;
Ok(DigestSha1::from_bytes(value)?)
}
}
#[cfg(feature = "sqlx")]
impl<'q, Db: Database> sqlx::Encode<'q, Db> for DigestSha1
where
Vec<u8>: sqlx::Encode<'q, Db>,
{
fn encode_by_ref(&self, buf: &mut <Db as database::HasArguments<'q>>::ArgumentBuffer) -> sqlx::encode::IsNull {
self.as_slice().to_vec().encode(buf)
}
}
impl TryFrom<&[u8]> for DigestSha1 {
type Error = DigestSha1FromBytesError;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
Self::from_bytes(value)
}
}
impl TryFrom<&str> for DigestSha1 {
type Error = ParseDigestSha1Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::from_hex(value)
}
}
impl From<DigestSha1> for ArrayString<40> {
fn from(value: DigestSha1) -> Self {
value.hex()
}
}
impl FromStr for DigestSha1 {
type Err = ParseDigestSha1Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::from_hex(input)
}
}
impl fmt::Debug for DigestSha1 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("DigestSha1").field(&self.hex()).finish()
}
}
impl fmt::Display for DigestSha1 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.hex(), f)
}
}
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(try_from = "&str", into = "ArrayString<64>")
)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DigestSha2([u8; 32]);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Error)]
#[error("invalid sha1 digest size, expected = 32, actual = {0}")]
pub struct DigestSha2FromBytesError(pub usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Error)]
pub enum ParseDigestSha2Error {
#[error("wrong length, expected 64 hex characters as input, actual = {0}")]
Length(usize),
#[error("input is not a lowercase hex string: found {character:?} at offset {offset}")]
InvalidCharacter { character: char, offset: usize },
}
impl DigestSha2 {
pub fn as_slice(&self) -> &[u8] {
&self.0
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, DigestSha2FromBytesError> {
match <[u8; 32]>::try_from(bytes) {
Ok(a) => Ok(Self(a)),
Err(_) => Err(DigestSha2FromBytesError(bytes.len())),
}
}
pub fn from_hex(input: &str) -> Result<Self, ParseDigestSha2Error> {
let mut bytes = [0u8; 32];
match hex::decode_to_slice(input, &mut bytes) {
Ok(()) => Ok(Self(bytes)),
Err(hex::FromHexError::InvalidHexCharacter { c, index }) => Err(ParseDigestSha2Error::InvalidCharacter {
character: c,
offset: index,
}),
Err(hex::FromHexError::OddLength | hex::FromHexError::InvalidStringLength) => {
Err(ParseDigestSha2Error::Length(input.len()))
}
}
}
pub fn hex(&self) -> ArrayString<64> {
let mut out = [0u8; 64];
hex::encode_to_slice(self.0, &mut out).expect("encoding to hex always succeeds");
ArrayString::from_byte_string(&out).expect("converting the hex byte string to an array string always succeeds")
}
pub fn digest(data: &[u8]) -> Self {
use sha2::{Digest, Sha256};
Self(Sha256::digest(data).into())
}
}
#[cfg(feature = "sqlx")]
impl sqlx::Type<Postgres> for DigestSha2 {
fn type_info() -> postgres::PgTypeInfo {
postgres::PgTypeInfo::with_name("digest_sha2")
}
fn compatible(ty: &postgres::PgTypeInfo) -> bool {
*ty == Self::type_info() || <&[u8] as sqlx::Type<Postgres>>::compatible(ty)
}
}
#[cfg(feature = "sqlx")]
impl<'r, Db: Database> sqlx::Decode<'r, Db> for DigestSha2
where
&'r [u8]: sqlx::Decode<'r, Db>,
{
fn decode(
value: <Db as database::HasValueRef<'r>>::ValueRef,
) -> Result<DigestSha2, Box<dyn std::error::Error + 'static + Send + Sync>> {
let value: &[u8] = <&[u8] as sqlx::Decode<Db>>::decode(value)?;
Ok(DigestSha2::from_bytes(value)?)
}
}
#[cfg(feature = "sqlx")]
impl<'q, Db: Database> sqlx::Encode<'q, Db> for DigestSha2
where
Vec<u8>: sqlx::Encode<'q, Db>,
{
fn encode_by_ref(&self, buf: &mut <Db as database::HasArguments<'q>>::ArgumentBuffer) -> sqlx::encode::IsNull {
self.as_slice().to_vec().encode(buf)
}
}
impl TryFrom<&[u8]> for DigestSha2 {
type Error = DigestSha2FromBytesError;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
Self::from_bytes(value)
}
}
impl TryFrom<&str> for DigestSha2 {
type Error = ParseDigestSha2Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::from_hex(value)
}
}
impl From<DigestSha2> for ArrayString<64> {
fn from(value: DigestSha2) -> Self {
value.hex()
}
}
impl FromStr for DigestSha2 {
type Err = ParseDigestSha2Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::from_hex(input)
}
}
impl fmt::Debug for DigestSha2 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("DigestSha2").field(&self.hex()).finish()
}
}
impl fmt::Display for DigestSha2 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.hex(), f)
}
}
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(try_from = "&str", into = "ArrayString<64>")
)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DigestSha3([u8; 32]);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Error)]
#[error("invalid sha1 digest size, expected = 32, actual = {0}")]
pub struct DigestSha3FromBytesError(pub usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Error)]
pub enum ParseDigestSha3Error {
#[error("wrong length, expected 64 hex characters as input, actual = {0}")]
Length(usize),
#[error("input is not a lowercase hex string: found {character:?} at offset {offset}")]
InvalidCharacter { character: char, offset: usize },
}
impl DigestSha3 {
pub fn as_slice(&self) -> &[u8] {
&self.0
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, DigestSha3FromBytesError> {
match <[u8; 32]>::try_from(bytes) {
Ok(a) => Ok(Self(a)),
Err(_) => Err(DigestSha3FromBytesError(bytes.len())),
}
}
pub fn from_hex(input: &str) -> Result<Self, ParseDigestSha3Error> {
let mut bytes = [0u8; 32];
match hex::decode_to_slice(input, &mut bytes) {
Ok(()) => Ok(Self(bytes)),
Err(hex::FromHexError::InvalidHexCharacter { c, index }) => Err(ParseDigestSha3Error::InvalidCharacter {
character: c,
offset: index,
}),
Err(hex::FromHexError::OddLength | hex::FromHexError::InvalidStringLength) => {
Err(ParseDigestSha3Error::Length(input.len()))
}
}
}
pub fn hex(&self) -> ArrayString<64> {
let mut out = [0u8; 64];
hex::encode_to_slice(self.0, &mut out).expect("encoding to hex always succeeds");
ArrayString::from_byte_string(&out).expect("converting the hex byte string to an array string always succeeds")
}
pub fn digest(data: &[u8]) -> Self {
use sha3::{Digest, Sha3_256};
Self(Sha3_256::digest(data).into())
}
}
#[cfg(feature = "sqlx")]
impl sqlx::Type<Postgres> for DigestSha3 {
fn type_info() -> postgres::PgTypeInfo {
postgres::PgTypeInfo::with_name("digest_sha3")
}
fn compatible(ty: &postgres::PgTypeInfo) -> bool {
*ty == Self::type_info() || <&[u8] as sqlx::Type<Postgres>>::compatible(ty)
}
}
#[cfg(feature = "sqlx")]
impl<'r, Db: Database> sqlx::Decode<'r, Db> for DigestSha3
where
&'r [u8]: sqlx::Decode<'r, Db>,
{
fn decode(
value: <Db as database::HasValueRef<'r>>::ValueRef,
) -> Result<DigestSha3, Box<dyn std::error::Error + 'static + Send + Sync>> {
let value: &[u8] = <&[u8] as sqlx::Decode<Db>>::decode(value)?;
Ok(DigestSha3::from_bytes(value)?)
}
}
#[cfg(feature = "sqlx")]
impl<'q, Db: Database> sqlx::Encode<'q, Db> for DigestSha3
where
Vec<u8>: sqlx::Encode<'q, Db>,
{
fn encode_by_ref(&self, buf: &mut <Db as database::HasArguments<'q>>::ArgumentBuffer) -> sqlx::encode::IsNull {
self.as_slice().to_vec().encode(buf)
}
}
impl TryFrom<&[u8]> for DigestSha3 {
type Error = DigestSha3FromBytesError;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
Self::from_bytes(value)
}
}
impl TryFrom<&str> for DigestSha3 {
type Error = ParseDigestSha3Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::from_hex(value)
}
}
impl From<DigestSha3> for ArrayString<64> {
fn from(value: DigestSha3) -> Self {
value.hex()
}
}
impl FromStr for DigestSha3 {
type Err = ParseDigestSha3Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::from_hex(input)
}
}
impl fmt::Debug for DigestSha3 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("DigestSha3").field(&self.hex()).finish()
}
}
impl fmt::Display for DigestSha3 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.hex(), f)
}
}
#[cfg(test)]
mod test {
mod digest_sha1 {
use crate::digest::{DigestSha1, ParseDigestSha1Error};
#[test]
fn from_hex_ok() {
let actual = DigestSha1::from_hex("27b664227bd281744d1449568fc6edb06361e747");
#[rustfmt::skip]
let expected = Ok(DigestSha1([
0x27, 0xb6, 0x64, 0x22, 0x7b, 0xd2, 0x81, 0x74,
0x4d, 0x14, 0x49, 0x56, 0x8f, 0xc6, 0xed, 0xb0,
0x63, 0x61, 0xe7, 0x47,
]));
assert_eq!(actual, expected);
}
#[test]
fn from_hex_empty() {
let actual = DigestSha1::from_hex("");
let expected = Err(ParseDigestSha1Error::Length(0));
assert_eq!(actual, expected);
}
#[test]
fn from_hex_too_long() {
let actual = DigestSha1::from_hex("27b664227bd281744d1449568fc6edb06361e7470000");
let expected = Err(ParseDigestSha1Error::Length(44));
assert_eq!(actual, expected);
}
#[test]
fn from_hex_odd_length() {
let actual = DigestSha1::from_hex("27b664227bd281744d1449568fc6edb06361e7470");
let expected = Err(ParseDigestSha1Error::Length(41));
assert_eq!(actual, expected);
}
#[test]
fn from_hex_invalid_char() {
let actual = DigestSha1::from_hex("27!664227bd281744d1449568fc6edb06361e747");
let expected = Err(ParseDigestSha1Error::InvalidCharacter {
character: '!',
offset: 2,
});
assert_eq!(actual, expected);
}
}
}