evobench_tools/serde_types/
git_hash.rs

1use std::{
2    fmt::{Debug, Display},
3    str::FromStr,
4};
5
6use anyhow::{Result, bail};
7use derive_more::From;
8use serde::de::Visitor;
9
10use crate::serde_types::git_reference::GitReference;
11
12fn decode_hex_digit(b: u8) -> Result<u8> {
13    if b >= b'0' && b <= b'9' {
14        Ok(b - b'0')
15    } else if b >= b'a' && b <= b'f' {
16        Ok(b - b'a' + 10)
17    } else if b >= b'A' && b <= b'F' {
18        Ok(b - b'A' + 10)
19    } else {
20        bail!("byte is not a hex digit: {b}")
21    }
22}
23
24fn decode_hex<const N: usize>(input: &[u8], output: &mut [u8; N]) -> Result<()> {
25    let n2 = 2 * N;
26    if input.len() != n2 {
27        bail!(
28            "wrong number of hex digits, expect {n2}, got {}",
29            input.len()
30        )
31    }
32    for i in 0..N {
33        output[i] = decode_hex_digit(input[i * 2])? * 16 + decode_hex_digit(input[i * 2 + 1])?;
34    }
35    Ok(())
36}
37
38#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, From)]
39pub struct GitHash([u8; 20]);
40
41impl GitHash {
42    pub fn to_reference(&self) -> GitReference {
43        self.to_string()
44            .parse()
45            .expect("git hashes are always references")
46    }
47}
48
49impl Debug for GitHash {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        f.write_str("GitHash!(\"")?;
52        Display::fmt(self, f)?;
53        f.write_str("\")")
54    }
55}
56
57/// Construct a `GitHash` instance (used as syntax in its `Debug` impl)
58#[macro_export]
59macro_rules! GitHash {
60    {$hash:expr} => { GitHash::try_from($hash.as_bytes()).unwrap() }
61}
62
63impl TryFrom<&[u8]> for GitHash {
64    type Error = anyhow::Error;
65
66    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
67        // let s = std::str::from_utf8(value)?;
68        if value.len() != 40 {
69            bail!(
70                "not a git hash of 40 hex bytes: {:?}",
71                String::from_utf8_lossy(value)
72            )
73        }
74        let mut bytes = [0; 20];
75        decode_hex(value, &mut bytes)?;
76        Ok(Self(bytes))
77    }
78}
79
80impl FromStr for GitHash {
81    type Err = anyhow::Error;
82
83    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
84        if s.chars().count() != s.len() {
85            bail!("not an ASCII string: {s:?}")
86        }
87        GitHash::try_from(s.as_bytes())
88    }
89}
90
91impl Display for GitHash {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        for b in self.0 {
94            f.write_fmt(format_args!("{:1x}{:1x}", b / 16, b & 15))?;
95        }
96        Ok(())
97    }
98}
99
100#[test]
101fn t_githash() -> Result<()> {
102    let s = "18fdd1625c4d98526736ea8e5047a4ca818de0b4";
103    let h1 = GitHash::try_from(s.as_bytes())?;
104    let h = GitHash!(s);
105    assert_eq!(h1, h);
106    assert_eq!(h.0[0], 0x18);
107    assert_eq!(h.0[1], 0xfd);
108    assert_eq!(h.0[2], 0xd1);
109    assert_eq!(format!("{h}"), s);
110    Ok(())
111}
112
113const ERR_MSG: &str = "a full hexadecimal Git hash";
114
115struct GitHashVisitor;
116impl<'de> Visitor<'de> for GitHashVisitor {
117    type Value = GitHash;
118
119    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
120        formatter.write_str(ERR_MSG)
121    }
122
123    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
124    where
125        E: serde::de::Error,
126    {
127        v.parse().map_err(E::custom)
128    }
129}
130
131impl<'de> serde::Deserialize<'de> for GitHash {
132    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
133        deserializer.deserialize_str(GitHashVisitor)
134    }
135}
136
137impl serde::Serialize for GitHash {
138    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
139    where
140        S: serde::Serializer,
141    {
142        serializer.serialize_str(&self.to_string())
143    }
144}