#![allow(dead_code, unused_mut)]
use serde::{
de::{Deserialize, Deserializer, Error as DeError, Visitor},
ser::{Serialize, Serializer},
};
use std::{
error::Error,
fmt::{Display, Formatter, Result as FmtResult},
str::FromStr,
};
const ANIMATED_KEY: &str = "a_";
const HASH_LEN: usize = 32;
#[derive(Debug)]
pub struct ImageHashParseError {
kind: ImageHashParseErrorType,
}
impl ImageHashParseError {
const FORMAT: Self = ImageHashParseError {
kind: ImageHashParseErrorType::Format,
};
#[must_use = "retrieving the type has no effect if left unused"]
pub const fn kind(&self) -> &ImageHashParseErrorType {
&self.kind
}
#[allow(clippy::unused_self)]
#[must_use = "consuming the error and retrieving the source has no effect if left unused"]
pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
None
}
#[must_use = "consuming the error into its parts has no effect if left unused"]
pub fn into_parts(
self,
) -> (
ImageHashParseErrorType,
Option<Box<dyn Error + Send + Sync>>,
) {
(self.kind, None)
}
const fn range(index: usize, value: u8) -> Self {
Self {
kind: ImageHashParseErrorType::Range { index, value },
}
}
}
impl Display for ImageHashParseError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self.kind {
ImageHashParseErrorType::Format => {
f.write_str("image hash isn't in a discord image hash format")
}
ImageHashParseErrorType::Range { index, value } => {
f.write_str("value (")?;
Display::fmt(&value, f)?;
f.write_str(") at encountered index (")?;
Display::fmt(&index, f)?;
f.write_str(") is not an acceptable value")
}
}
}
}
impl Error for ImageHashParseError {}
#[derive(Debug)]
#[non_exhaustive]
pub enum ImageHashParseErrorType {
Format,
Range {
index: usize,
value: u8,
},
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct ImageHash {
animated: bool,
bytes: [u8; 16],
}
impl ImageHash {
pub const CLYDE: Self = Self {
animated: true,
bytes: {
let mut bytes = [0; 16];
bytes[0] = b'c';
bytes[1] = b'l';
bytes[2] = b'y';
bytes[3] = b'd';
bytes[4] = b'e';
bytes
},
};
pub const fn new(bytes: [u8; 16], animated: bool) -> Self {
Self { animated, bytes }
}
pub const fn parse(value: &[u8]) -> Result<Self, ImageHashParseError> {
const DIGITS_ALLOCATED: u8 = 10;
if Self::is_clyde(value) {
return Ok(Self::CLYDE);
}
let animated = Self::starts_with(value, ANIMATED_KEY.as_bytes());
let mut seeking_idx = if animated { ANIMATED_KEY.len() } else { 0 };
let mut storage_idx = 15;
if value.len() - seeking_idx != HASH_LEN {
return Err(ImageHashParseError::FORMAT);
}
let mut bytes = [0; 16];
while seeking_idx < value.len() {
let byte_left = match value[seeking_idx] {
byte @ b'0'..=b'9' => byte - b'0',
byte @ b'a'..=b'f' => byte - b'a' + DIGITS_ALLOCATED,
other => return Err(ImageHashParseError::range(seeking_idx, other)),
};
seeking_idx += 1;
let byte_right = match value[seeking_idx] {
byte @ b'0'..=b'9' => byte - b'0',
byte @ b'a'..=b'f' => byte - b'a' + DIGITS_ALLOCATED,
other => return Err(ImageHashParseError::range(seeking_idx, other)),
};
bytes[storage_idx] = (byte_left << 4) | byte_right;
seeking_idx += 1;
storage_idx = storage_idx.saturating_sub(1);
}
Ok(Self { animated, bytes })
}
pub const fn bytes(self) -> [u8; 16] {
self.bytes
}
pub const fn is_animated(self) -> bool {
self.animated
}
pub const fn nibbles(self) -> Nibbles {
Nibbles::new(self)
}
const fn is_clyde(value: &[u8]) -> bool {
if value.len() < 5 {
return false;
}
value[0] == b'c'
&& value[1] == b'l'
&& value[2] == b'y'
&& value[3] == b'd'
&& value[4] == b'e'
}
const fn starts_with(haystack: &[u8], needle: &[u8]) -> bool {
if needle.len() > haystack.len() {
return false;
}
let mut idx = 0;
while idx < needle.len() {
if haystack[idx] != needle[idx] {
return false;
}
idx += 1;
}
true
}
}
impl<'de> Deserialize<'de> for ImageHash {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct ImageHashVisitor;
impl Visitor<'_> for ImageHashVisitor {
type Value = ImageHash;
fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
f.write_str("image hash")
}
fn visit_str<E: DeError>(self, v: &str) -> Result<Self::Value, E> {
ImageHash::parse(v.as_bytes()).map_err(DeError::custom)
}
}
deserializer.deserialize_any(ImageHashVisitor)
}
}
impl Display for ImageHash {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
if *self == Self::CLYDE {
return f.write_str("clyde");
}
if self.is_animated() {
f.write_str(ANIMATED_KEY)?;
}
for hex_value in self.nibbles() {
let legible = char::from(hex_value);
Display::fmt(&legible, f)?;
}
Ok(())
}
}
impl FromStr for ImageHash {
type Err = ImageHashParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s.as_bytes())
}
}
impl Serialize for ImageHash {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
impl TryFrom<&[u8]> for ImageHash {
type Error = ImageHashParseError;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
Self::parse(value)
}
}
impl TryFrom<&str> for ImageHash {
type Error = ImageHashParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::try_from(value.as_bytes())
}
}
#[derive(Debug)]
pub struct Nibbles {
idx: usize,
inner: ImageHash,
}
impl Nibbles {
const fn new(inner: ImageHash) -> Self {
Self {
idx: usize::MAX,
inner,
}
}
const fn advance_idx_by(&mut self, by: usize) {
self.idx = if self.idx == usize::MAX {
0
} else {
let mut new_idx = self.idx.saturating_add(by);
if new_idx == usize::MAX {
new_idx -= 1;
}
new_idx
}
}
const fn byte(&self) -> Option<u8> {
const BITS_IN_HALF_BYTE: u8 = 4;
const BYTE_ARRAY_BOUNDARY: usize = HASH_LEN - 1;
const RIGHT_MASK: u8 = (1 << BITS_IN_HALF_BYTE) - 1;
if self.idx >= HASH_LEN {
return None;
}
let (byte, left) = (
(BYTE_ARRAY_BOUNDARY - self.idx) / 2,
self.idx.is_multiple_of(2),
);
let store = self.inner.bytes[byte];
let bits = if left {
store >> BITS_IN_HALF_BYTE
} else {
store & RIGHT_MASK
};
Some(Self::nibble(bits))
}
const fn nibble(value: u8) -> u8 {
if value < 10 {
b'0' + value
} else {
b'a' + (value - 10)
}
}
}
impl DoubleEndedIterator for Nibbles {
fn next_back(&mut self) -> Option<Self::Item> {
if self.idx == usize::MAX {
return None;
}
self.idx = self.idx.checked_sub(1)?;
self.byte()
}
}
impl ExactSizeIterator for Nibbles {
fn len(&self) -> usize {
HASH_LEN
}
}
impl Iterator for Nibbles {
type Item = u8;
fn next(&mut self) -> Option<Self::Item> {
self.advance_idx_by(1);
self.byte()
}
fn nth(&mut self, n: usize) -> Option<Self::Item> {
self.advance_idx_by(n.saturating_add(1));
self.byte()
}
fn size_hint(&self) -> (usize, Option<usize>) {
(HASH_LEN, Some(HASH_LEN))
}
}
#[cfg(test)]
mod tests {
use super::{ImageHash, ImageHashParseError, ImageHashParseErrorType, Nibbles};
use static_assertions::assert_impl_all;
use std::{
error::Error,
fmt::{Debug, Display},
hash::Hash,
};
assert_impl_all!(
Nibbles: Debug,
DoubleEndedIterator,
ExactSizeIterator,
Iterator,
Send,
Sync
);
assert_impl_all!(ImageHashParseErrorType: Debug, Send, Sync);
assert_impl_all!(ImageHashParseError: Error, Send, Sync);
assert_impl_all!(
ImageHash: Clone,
Debug,
Display,
Eq,
Hash,
PartialEq,
Send,
Sync
);
#[test]
fn new() -> Result<(), ImageHashParseError> {
let source = ImageHash::parse(b"85362c0262ef125a1182b1fad66b6a89")?;
let (bytes, animated) = (source.bytes(), source.is_animated());
let reconstructed = ImageHash::new(bytes, animated);
assert_eq!(reconstructed, source);
Ok(())
}
#[test]
fn parse() -> Result<(), ImageHashParseError> {
let actual = ImageHash::parse(b"77450a7713f093adaebab32b18dacc46")?;
let expected = [
70, 204, 218, 24, 43, 179, 186, 174, 173, 147, 240, 19, 119, 10, 69, 119,
];
assert_eq!(actual.bytes(), expected);
Ok(())
}
#[test]
fn display() -> Result<(), ImageHashParseError> {
assert_eq!(
"58ec815c650e72f8eb31eec52e54b3b5",
ImageHash::parse(b"58ec815c650e72f8eb31eec52e54b3b5")?.to_string()
);
assert_eq!(
"a_e382aeb1574bf3e4fe852f862bc4919c",
ImageHash::parse(b"a_e382aeb1574bf3e4fe852f862bc4919c")?.to_string()
);
Ok(())
}
#[test]
fn parse_format() {
const INPUTS: &[&[u8]] = &[
b"not correct length",
b"",
b"a_",
b"a_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
];
for input in INPUTS {
assert!(matches!(
ImageHash::parse(input).unwrap_err().kind(),
&ImageHashParseErrorType::Format
));
}
}
#[test]
fn parse_range() {
let mut input = [b'a'; 32];
input[17] = b'-';
assert!(matches!(
ImageHash::parse(&input).unwrap_err().kind(),
ImageHashParseErrorType::Range {
index: 17,
value: b'-',
}
));
}
#[test]
fn nibbles() -> Result<(), ImageHashParseError> {
const INPUT: &[u8] = b"39eb706d6fbaeb22837c350993b97b42";
let hash = ImageHash::parse(INPUT)?;
let mut iter = hash.nibbles();
for byte in INPUT.iter().copied() {
assert_eq!(Some(byte), iter.next());
}
assert!(iter.next().is_none());
Ok(())
}
#[test]
fn nibbles_double_ended() -> Result<(), ImageHashParseError> {
const INPUT: &[u8] = b"e72bbdec903c420b7aa9c45fc7994ac8";
let hash = ImageHash::parse(INPUT)?;
let mut iter = hash.nibbles();
assert!(iter.next_back().is_none());
assert_eq!(Some(b'e'), iter.next());
assert!(iter.next_back().is_none());
assert_eq!(Some(b'e'), iter.nth(5));
assert_eq!(Some(b'8'), iter.nth(24));
assert!(iter.next().is_none());
assert_eq!(Some(b'8'), iter.next_back());
assert!(iter.next().is_none());
Ok(())
}
#[test]
fn is_animated() -> Result<(), ImageHashParseError> {
assert!(ImageHash::parse(b"a_06c16474723fe537c283b8efa61a30c8")?.is_animated());
assert!(!ImageHash::parse(b"06c16474723fe537c283b8efa61a30c8")?.is_animated());
Ok(())
}
#[test]
fn clyde() -> Result<(), ImageHashParseError> {
assert_eq!(ImageHash::CLYDE, ImageHash::parse(b"clyde")?);
serde_test::assert_tokens(&ImageHash::CLYDE, &[serde_test::Token::Str("clyde")]);
Ok(())
}
}