1crate::cow_str_newtype! {
4 pub struct CommitId, CommitIdError(InvalidCommitId), "invalid commit id"
13}
14
15impl CommitId {
16 const fn validate(input: &str) -> Result<(), CommitIdError> {
17 let bytes = input.as_bytes();
18 let len = bytes.len();
19
20 if len == 0 {
21 return Err(CommitIdError(InvalidCommitId::Empty));
22 }
23 if len != 40 && len != 64 {
24 return Err(CommitIdError(InvalidCommitId::InvalidLength));
25 }
26
27 let mut index = 0;
28 while index < bytes.len() {
29 if !bytes[index].is_ascii_hexdigit() {
30 return Err(CommitIdError(InvalidCommitId::ContainsNonHexCharacter));
31 }
32 index += 1;
33 }
34
35 Ok(())
36 }
37}
38
39#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
41pub enum InvalidCommitId {
42 #[error("commit id cannot be empty")]
43 Empty,
44 #[error("commit id must be 40 (SHA-1) or 64 (SHA-256) hex characters")]
45 InvalidLength,
46 #[error("commit id must contain only hex characters")]
47 ContainsNonHexCharacter,
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53
54 const SHA1: &str = "0123456789abcdef0123456789abcdef01234567";
55 const SHA256: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
56
57 #[test]
58 fn test_valid_sha1() {
59 let id: CommitId = SHA1.parse().unwrap();
60 assert_eq!(id.as_str(), SHA1);
61 }
62
63 #[test]
64 fn test_valid_sha256() {
65 let id: CommitId = SHA256.parse().unwrap();
66 assert_eq!(id.as_str(), SHA256);
67 }
68
69 #[test]
70 fn test_uppercase_accepted() {
71 let id: CommitId = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF".parse().unwrap();
72 assert_eq!(id.as_str(), "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF");
73 }
74
75 #[test]
76 fn test_empty() {
77 assert!(matches!(
78 "".parse::<CommitId>(),
79 Err(CommitIdError(InvalidCommitId::Empty))
80 ));
81 }
82
83 #[test]
84 fn test_abbreviated_rejected() {
85 assert!(matches!(
86 "abc1234".parse::<CommitId>(),
87 Err(CommitIdError(InvalidCommitId::InvalidLength))
88 ));
89 }
90
91 #[test]
92 fn test_wrong_length() {
93 assert!(matches!(
94 "abc".parse::<CommitId>(),
95 Err(CommitIdError(InvalidCommitId::InvalidLength))
96 ));
97 assert!(matches!(
99 "0123456789abcdef0123456789abcdef012345670".parse::<CommitId>(),
100 Err(CommitIdError(InvalidCommitId::InvalidLength))
101 ));
102 }
103
104 #[test]
105 fn test_non_hex() {
106 let bad = "g123456789abcdef0123456789abcdef01234567";
108 assert_eq!(bad.len(), 40);
109 assert!(matches!(
110 bad.parse::<CommitId>(),
111 Err(CommitIdError(InvalidCommitId::ContainsNonHexCharacter))
112 ));
113 }
114
115 #[test]
116 fn test_from_static_or_panic() {
117 let id = CommitId::from_static_or_panic(SHA1);
118 assert_eq!(id.as_str(), SHA1);
119 }
120
121 #[test]
122 fn test_serde_roundtrip() {
123 let id: CommitId = SHA1.parse().unwrap();
124 let json = serde_json::to_string(&id).unwrap();
125 assert_eq!(json, format!("\"{SHA1}\""));
126 let back: CommitId = serde_json::from_str(&json).unwrap();
127 assert_eq!(back.as_str(), SHA1);
128 }
129}