git_bug/replica/entity/id/
mod.rs

1// git-bug-rs - A rust library for interfacing with git-bug repositories
2//
3// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
4// SPDX-License-Identifier: GPL-3.0-or-later
5//
6// This file is part of git-bug-rs/git-gub.
7//
8// You should have received a copy of the License along with this program.
9// If not, see <https://www.gnu.org/licenses/agpl.txt>.
10
11//! [`Ids`][`Id`] are needed to provide globally consistent references to
12//! [`Entities`][`super::Entity`].
13//!
14//! The use will probably only interface with [`Prefixes`][`IdPrefix`].
15
16use std::fmt::Display;
17
18use combined_id::CombinedId;
19use prefix::IdPrefix;
20use serde::{Deserialize, Serialize};
21
22pub mod combined_id;
23pub mod entity_id;
24pub mod prefix;
25
26/// The Id is an identifier for an entity or part of an entity
27#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
28pub struct Id {
29    /// The 64 hex-chars composing the underlying sha256 hash.
30    sha256_value: [u8; 32],
31}
32
33impl Display for Id {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        let mut buf = [0; 64];
36        write!(f, "{}", self.hex_to_buf(&mut buf))
37    }
38}
39
40impl std::fmt::Debug for Id {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        f.write_str("Sha256(")?;
43        for b in self.as_bytes() {
44            write!(f, "{b:02x}")?;
45        }
46        f.write_str(")")
47    }
48}
49
50#[allow(missing_docs)]
51pub mod decode {
52    use std::str::FromStr;
53
54    #[derive(Debug, thiserror::Error)]
55    pub enum Error {
56        #[error("Your hex input buffer was not 64 bytes long, but: {0}")]
57        InvalidLen(usize),
58
59        #[error("Your hex input contained an invalid char. The buffer is: '{0:?}'")]
60        InvalidChar(Vec<u8>),
61    }
62
63    impl FromStr for super::Id {
64        type Err = Error;
65
66        fn from_str(s: &str) -> Result<Self, Self::Err> {
67            Self::from_hex(s.as_bytes())
68        }
69    }
70
71    impl TryFrom<&str> for super::Id {
72        type Error = Error;
73
74        fn try_from(value: &str) -> Result<Self, Self::Error> {
75            <Self as FromStr>::from_str(value)
76        }
77    }
78    impl TryFrom<String> for super::Id {
79        type Error = Error;
80
81        fn try_from(value: String) -> Result<Self, Self::Error> {
82            <Self as FromStr>::from_str(&value)
83        }
84    }
85}
86
87impl From<Id> for String {
88    fn from(value: Id) -> Self {
89        value.to_string()
90    }
91}
92
93impl Id {
94    /// Parse the `buffer` as a sequence of hex bytes from which to decode use.
95    ///
96    /// # Note
97    /// This means that the length of the buffer must be 64.
98    ///
99    /// # Errors
100    /// This will return an error, if the `buffer` cannot be interpreted as
101    /// sequence of hex bytes, or is too long/short.
102    pub fn from_hex(buffer: &[u8]) -> Result<Self, decode::Error> {
103        if buffer.len() != 64 {
104            return Err(decode::Error::InvalidLen(buffer.len()));
105        }
106
107        let mut buf = [0; 32];
108        faster_hex::hex_decode(buffer, &mut buf).map_err(|err| match err {
109            faster_hex::Error::InvalidChar => decode::Error::InvalidChar(buffer.to_owned()),
110            faster_hex::Error::InvalidLength(len) => {
111                unreachable!("Bug: Id Length is invalid but was checked: {len}")
112            }
113            faster_hex::Error::Overflow => unreachable!("We allocated enough"),
114        })?;
115
116        Ok(Self { sha256_value: buf })
117    }
118
119    /// Instantiate this Id from already decoded hex bytes.
120    ///
121    /// These bytes can be the result of a sha256 hashing operation.
122    ///
123    /// # Panics
124    /// If your bytes are not exactly 32 elements long.
125    pub(crate) fn from_sha256_hash(input: &[u8]) -> Self {
126        let mut buffer = [0; 32];
127        buffer.copy_from_slice(input);
128        Self {
129            sha256_value: buffer,
130        }
131    }
132
133    fn as_bytes(&self) -> &[u8] {
134        &self.sha256_value
135    }
136
137    /// Return the raw byte slice representing this [`Id`].
138    #[must_use]
139    pub fn as_slice(&self) -> &[u8] {
140        &self.sha256_value
141    }
142
143    /// Write ourselves to the `out` in hexadecimal notation, returning the
144    /// hex-string ready for display.
145    ///
146    /// # Panics
147    /// If the buffer isn't big enough to hold twice as many bytes as stored (32
148    /// * 2).
149    pub fn hex_to_buf<'a>(&self, buf: &'a mut [u8]) -> &'a mut str {
150        let num_hex_bytes = self.sha256_value.len() * 2;
151        faster_hex::hex_encode(self.as_bytes(), &mut buf[..num_hex_bytes]).expect("We can count")
152    }
153
154    /// Shorten an [`Id`] to the required length, so that it stays unique, but
155    /// is still short enough for human consumption.
156    #[must_use]
157    pub fn shorten(&self) -> IdPrefix {
158        (*self).into()
159    }
160
161    /// Combine this id with another [`Id`].
162    ///
163    /// This is useful, if you want to get a shorten prefix that can be used to direct to a comment
164    /// (for example).
165    ///
166    /// # Note
167    /// The underlying algorithm assumes that there are more
168    /// [`Ids`][`Id`] for the type of `self` than for `other.`
169    #[must_use]
170    pub fn combine_with(self, other: Id) -> CombinedId {
171        CombinedId {
172            primary_id: self,
173            secondary_id: other,
174        }
175    }
176}