1#![no_std]
2
3#[cfg(doc)]
68extern crate std as doc_std;
69
70extern crate alloc;
71
72#[cfg(not(feature = "sha1"))]
75compile_error!("The `sha1` feature is required.");
76
77const SHA1_DIGEST_LEN: usize = 20;
78
79#[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Copy)]
80#[non_exhaustive]
81pub enum Oid {
82 Sha1([u8; SHA1_DIGEST_LEN]),
83}
84
85impl Oid {
90 pub fn from_sha1(digest: [u8; SHA1_DIGEST_LEN]) -> Self {
91 Self::Sha1(digest)
92 }
93
94 pub fn into_sha1(&self) -> Option<[u8; SHA1_DIGEST_LEN]> {
95 match self {
96 Oid::Sha1(digest) => Some(*digest),
97 }
98 }
99
100 pub fn sha1_zero() -> Self {
101 Self::Sha1([0u8; SHA1_DIGEST_LEN])
102 }
103}
104
105impl Oid {
107 pub fn is_zero(&self) -> bool {
110 match self {
111 Oid::Sha1(ref array) => array.iter().all(|b| *b == 0),
112 }
113 }
114}
115
116impl AsRef<[u8]> for Oid {
117 fn as_ref(&self) -> &[u8] {
118 match self {
119 Oid::Sha1(ref array) => array,
120 }
121 }
122}
123
124impl From<Oid> for alloc::boxed::Box<[u8]> {
125 fn from(oid: Oid) -> Self {
126 match oid {
127 Oid::Sha1(array) => alloc::boxed::Box::new(array),
128 }
129 }
130}
131
132pub mod str {
133 use super::{Oid, SHA1_DIGEST_LEN};
134 use core::str;
135
136 pub(super) const SHA1_DIGEST_STR_LEN: usize = SHA1_DIGEST_LEN * 2;
138
139 impl str::FromStr for Oid {
140 type Err = error::ParseOidError;
141
142 fn from_str(s: &str) -> Result<Self, Self::Err> {
143 use error::ParseOidError::*;
144
145 let len = s.len();
146 if len != SHA1_DIGEST_STR_LEN {
147 return Err(Len(len));
148 }
149
150 let mut bytes = [0u8; SHA1_DIGEST_LEN];
151 for i in 0..SHA1_DIGEST_LEN {
152 bytes[i] = u8::from_str_radix(&s[i * 2..=i * 2 + 1], 16)
153 .map_err(|source| At { index: i, source })?;
154 }
155
156 Ok(Self::Sha1(bytes))
157 }
158 }
159
160 pub mod error {
161 use core::{fmt, num};
162
163 use super::SHA1_DIGEST_STR_LEN;
164
165 pub enum ParseOidError {
166 Len(usize),
167 At {
168 index: usize,
169 source: num::ParseIntError,
170 },
171 }
172
173 impl fmt::Display for ParseOidError {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 use ParseOidError::*;
176 match self {
177 Len(len) => {
178 write!(f, "invalid length (have {len}, want {SHA1_DIGEST_STR_LEN})")
179 }
180 At { index, source } => write!(
181 f,
182 "parse error at byte {index} (characters {} and {}): {source}",
183 index * 2,
184 index * 2 + 1
185 ),
186 }
187 }
188 }
189
190 impl fmt::Debug for ParseOidError {
191 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192 fmt::Display::fmt(self, f)
193 }
194 }
195
196 impl core::error::Error for ParseOidError {
197 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
198 match self {
199 ParseOidError::At { source, .. } => Some(source),
200 _ => None,
201 }
202 }
203 }
204 }
205
206 pub use error::ParseOidError;
207
208 #[cfg(test)]
209 mod test {
210 use super::*;
211 use alloc::string::ToString;
212 use qcheck_macros::quickcheck;
213
214 #[test]
215 fn fixture() {
216 assert_eq!(
217 "123456789abcdef0123456789abcdef012345678"
218 .parse::<Oid>()
219 .unwrap(),
220 Oid::from_sha1([
221 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a,
222 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78,
223 ])
224 );
225 }
226
227 #[test]
228 fn zero() {
229 assert_eq!(
230 "0000000000000000000000000000000000000000"
231 .parse::<Oid>()
232 .unwrap(),
233 Oid::sha1_zero()
234 );
235 }
236
237 #[quickcheck]
238 fn git2_roundtrip(oid: Oid) {
239 let other = git2::Oid::from(oid);
240 let other = other.to_string();
241 let other = other.parse::<Oid>().unwrap();
242 assert_eq!(oid, other);
243 }
244
245 #[quickcheck]
246 fn gix_roundrip(oid: Oid) {
247 let other = gix_hash::ObjectId::from(oid);
248 let other = other.to_string();
249 let other = other.parse::<Oid>().unwrap();
250 assert_eq!(oid, other);
251 }
252 }
253}
254
255mod fmt {
256 use alloc::format;
257 use core::fmt;
258
259 use super::Oid;
260
261 impl fmt::Display for Oid {
262 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263 match self {
264 Oid::Sha1(digest) =>
265 format!(
269 "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
270 unsafe { digest.get_unchecked(0) },
271 unsafe { digest.get_unchecked(1) },
272 unsafe { digest.get_unchecked(2) },
273 unsafe { digest.get_unchecked(3) },
274 unsafe { digest.get_unchecked(4) },
275 unsafe { digest.get_unchecked(5) },
276 unsafe { digest.get_unchecked(6) },
277 unsafe { digest.get_unchecked(7) },
278 unsafe { digest.get_unchecked(8) },
279 unsafe { digest.get_unchecked(9) },
280 unsafe { digest.get_unchecked(10) },
281 unsafe { digest.get_unchecked(11) },
282 unsafe { digest.get_unchecked(12) },
283 unsafe { digest.get_unchecked(13) },
284 unsafe { digest.get_unchecked(14) },
285 unsafe { digest.get_unchecked(15) },
286 unsafe { digest.get_unchecked(16) },
287 unsafe { digest.get_unchecked(17) },
288 unsafe { digest.get_unchecked(18) },
289 unsafe { digest.get_unchecked(19) },
290 ).fmt(f)
291 }
292 }
293 }
294
295 impl fmt::Debug for Oid {
296 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297 fmt::Display::fmt(self, f)
298 }
299 }
300
301 #[cfg(test)]
302 mod test {
303 use super::*;
304 use alloc::string::ToString;
305 use qcheck_macros::quickcheck;
306
307 #[test]
308 fn fixture() {
309 assert_eq!(
310 Oid::from_sha1([
311 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a,
312 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78,
313 ])
314 .to_string(),
315 "123456789abcdef0123456789abcdef012345678"
316 );
317 }
318
319 #[test]
320 fn zero() {
321 assert_eq!(
322 Oid::sha1_zero().to_string(),
323 "0000000000000000000000000000000000000000"
324 );
325 }
326
327 #[quickcheck]
328 fn git2(oid: Oid) {
329 assert_eq!(oid.to_string(), git2::Oid::from(oid).to_string());
330 }
331
332 #[quickcheck]
333 fn gix(oid: Oid) {
334 assert_eq!(oid.to_string(), gix_hash::ObjectId::from(oid).to_string());
335 }
336 }
337}
338
339#[cfg(feature = "std")]
340mod std {
341 extern crate std;
342
343 use super::Oid;
344
345 mod hash {
346 use std::hash;
347
348 use super::*;
349
350 #[allow(clippy::derived_hash_with_manual_eq)]
351 impl hash::Hash for Oid {
352 fn hash<H: hash::Hasher>(&self, state: &mut H) {
353 let bytes: &[u8] = self.as_ref();
354 std::hash::Hash::hash(bytes, state)
355 }
356 }
357 }
358}
359
360#[cfg(any(feature = "gix", test))]
361mod gix {
362 use gix_hash::ObjectId as Other;
363
364 use super::Oid;
365
366 impl From<Other> for Oid {
367 fn from(other: Other) -> Self {
368 match other {
369 Other::Sha1(digest) => Self::Sha1(digest),
370 _ => panic!("unexpected SHA variant was returned for `gix_hash::ObjectId`"),
371 }
372 }
373 }
374
375 impl From<Oid> for Other {
376 fn from(oid: Oid) -> Other {
377 match oid {
378 Oid::Sha1(digest) => Other::Sha1(digest),
379 }
380 }
381 }
382
383 impl core::cmp::PartialEq<Other> for Oid {
384 fn eq(&self, other: &Other) -> bool {
385 match (self, other) {
386 (Oid::Sha1(a), Other::Sha1(b)) => a == b,
387 _ => panic!("unexpected SHA variant was returned for `gix_hash::ObjectId`"),
388 }
389 }
390 }
391
392 impl AsRef<gix_hash::oid> for Oid {
393 fn as_ref(&self) -> &gix_hash::oid {
394 match self {
395 Oid::Sha1(digest) => gix_hash::oid::from_bytes_unchecked(digest),
396 }
397 }
398 }
399
400 #[cfg(test)]
401 mod test {
402 use super::*;
403 use gix_hash::Kind;
404
405 #[test]
406 fn zero() {
407 assert!(Oid::sha1_zero() == Other::null(Kind::Sha1));
408 }
409 }
410}
411
412#[cfg(any(feature = "git2", test))]
413mod git2 {
414 use ::git2::Oid as Other;
415
416 use super::*;
417
418 const EXPECT: &str = "git2::Oid must be exactly 20 bytes long";
419
420 impl From<Other> for Oid {
421 fn from(other: Other) -> Self {
422 Self::Sha1(other.as_bytes().try_into().expect(EXPECT))
423 }
424 }
425
426 impl From<Oid> for Other {
427 fn from(oid: Oid) -> Self {
428 match oid {
429 Oid::Sha1(array) => Other::from_bytes(&array).expect(EXPECT),
430 }
431 }
432 }
433
434 impl From<&Oid> for Other {
435 fn from(oid: &Oid) -> Self {
436 match oid {
437 Oid::Sha1(array) => Other::from_bytes(array).expect(EXPECT),
438 }
439 }
440 }
441
442 impl core::cmp::PartialEq<Other> for Oid {
443 fn eq(&self, other: &Other) -> bool {
444 other.as_bytes() == AsRef::<[u8]>::as_ref(&self)
445 }
446 }
447
448 #[cfg(test)]
449 mod test {
450 use super::*;
451
452 #[test]
453 fn zero() {
454 assert!(Oid::sha1_zero() == Other::zero());
455 }
456 }
457}
458
459#[cfg(any(test, feature = "qcheck"))]
460mod test {
461 mod qcheck {
462 use ::qcheck::{Arbitrary, Gen};
463
464 use crate::*;
465
466 impl Arbitrary for Oid {
467 fn arbitrary(g: &mut Gen) -> Self {
468 let slice = [0u8; SHA1_DIGEST_LEN];
469 g.fill(slice);
470 Self::Sha1(slice)
471 }
472 }
473 }
474}
475
476#[cfg(feature = "serde")]
477mod serde {
478 mod ser {
479 use ::serde::ser;
480
481 use crate::*;
482
483 impl ser::Serialize for Oid {
484 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
485 where
486 S: ser::Serializer,
487 {
488 serializer.collect_str(self)
489 }
490 }
491 }
492
493 mod de {
494 use core::fmt;
495
496 use ::serde::de;
497
498 use crate::*;
499
500 impl<'de> de::Deserialize<'de> for Oid {
501 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
502 where
503 D: de::Deserializer<'de>,
504 {
505 struct OidVisitor;
506
507 impl<'de> de::Visitor<'de> for OidVisitor {
508 type Value = Oid;
509
510 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
511 use crate::str::SHA1_DIGEST_STR_LEN;
512 write!(f, "a Git object identifier (SHA-1 digest in hexadecimal notation; {SHA1_DIGEST_STR_LEN} characters; {SHA1_DIGEST_LEN} bytes)")
513 }
514
515 fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
516 where
517 E: de::Error,
518 {
519 s.parse().map_err(de::Error::custom)
520 }
521 }
522
523 deserializer.deserialize_str(OidVisitor)
524 }
525 }
526 }
527}
528
529#[cfg(feature = "radicle-git-ref-format")]
530mod radicle_git_ref_format {
531 use ::radicle_git_ref_format::{Component, RefString};
532
533 use super::*;
534
535 impl From<&Oid> for Component<'_> {
536 fn from(id: &Oid) -> Self {
537 Component::from_refstr(RefString::from(id))
538 .expect("Git object identifiers are valid component strings")
539 }
540 }
541
542 impl From<&Oid> for RefString {
543 fn from(id: &Oid) -> Self {
544 RefString::try_from(alloc::format!("{id}"))
545 .expect("Git object identifiers are valid reference strings")
546 }
547 }
548}
549
550#[cfg(feature = "schemars")]
551mod schemars {
552 use alloc::{borrow::Cow, format};
553
554 use ::schemars::{json_schema, JsonSchema, Schema, SchemaGenerator};
555
556 use super::Oid;
557
558 impl JsonSchema for Oid {
559 fn schema_name() -> Cow<'static, str> {
560 "Oid".into()
561 }
562
563 fn schema_id() -> Cow<'static, str> {
564 concat!(module_path!(), "::Oid").into()
565 }
566
567 fn json_schema(_: &mut SchemaGenerator) -> Schema {
568 use crate::{str::SHA1_DIGEST_STR_LEN, SHA1_DIGEST_LEN};
569 json_schema!({
570 "description": format!("A Git object identifier (SHA-1 digest in hexadecimal notation; {SHA1_DIGEST_STR_LEN} characters; {SHA1_DIGEST_LEN} bytes)"),
571 "type": "string",
572 "maxLength": SHA1_DIGEST_STR_LEN,
573 "minLength": SHA1_DIGEST_STR_LEN,
574 "pattern": format!("^[0-9a-fA-F]{{{SHA1_DIGEST_STR_LEN}}}$"),
575 })
576 }
577 }
578}