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