evobench_tools/serde_types/
git_hash.rs1use 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#[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 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}