git_bug/replica/entity/id/
prefix.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//! A shortened from of an [`Id`][`super::Id`]
12
13use std::fmt::{Display, Write};
14
15use gix::Repository;
16
17use super::{Id, entity_id::EntityId};
18use crate::replica::{
19    entity::{Entity, EntityRead},
20    entity_iter::EntityIdIter,
21};
22
23/// A shortened from of an [`Id`][`super::Id`].
24///
25/// This is meant for Human interaction.
26#[derive(Debug, Clone, Copy)]
27pub struct IdPrefix {
28    /// The first 4 hex-chars are always required in a Prefix.
29    first: [u8; Self::REQUIRED_LENGTH],
30
31    /// The other hex chars composing this [`IdPrefix`].
32    /// These could be added to make this Prefix more unique.
33    ///
34    /// They are padded with [`None`] to fill the array.
35    rest: [Option<u8>; 32 - Self::REQUIRED_LENGTH],
36
37    /// If the rest is not even, this is hex decoded value of the last element
38    /// in rest.
39    ///
40    /// Effectively this just stores a u4 (because a hex char can max be 15). As
41    /// such, the leading four bits are guaranteed to be 0.
42    uneven_offset: Option<u8>,
43}
44
45impl Display for IdPrefix {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        // TODO(@bpeetz): Ideally this function would actually check if 7 hex-chars are
48        // enough. <2025-04-19>
49
50        let mut buffer = [0; Self::REQUIRED_LENGTH * 2];
51        f.write_str(
52            &faster_hex::hex_encode(&self.first, &mut buffer).expect("We can count")
53                [..Self::REQUIRED_LENGTH * 2],
54        )?;
55
56        // Try to write at least 7 chars (i.e., try to take 7 - <already printed> chars).
57        for next in self
58            .rest
59            .iter()
60            .filter_map(|a| a.as_ref())
61            .chain(self.uneven_offset.iter())
62            .flat_map(|value| {
63                let second = value & 0xF;
64                let first = (value >> 4) & 0xF;
65
66                assert!(first < 16);
67                assert!(second < 16);
68
69                [first, second]
70            })
71            .take(7 - (Self::REQUIRED_LENGTH * 2))
72        {
73            f.write_char(encode_hex(next))?;
74        }
75
76        Ok(())
77    }
78}
79
80impl From<Id> for IdPrefix {
81    fn from(value: Id) -> Self {
82        let mut first = [0; Self::REQUIRED_LENGTH];
83        let mut rest = [None; 32 - Self::REQUIRED_LENGTH];
84
85        first.copy_from_slice(&value.as_bytes()[..Self::REQUIRED_LENGTH]);
86        for (slot, byte) in rest
87            .iter_mut()
88            .zip(&value.as_bytes()[Self::REQUIRED_LENGTH..])
89        {
90            *slot = Some(*byte);
91        }
92
93        Self {
94            first,
95            rest,
96            uneven_offset: None,
97        }
98    }
99}
100
101impl IdPrefix {
102    /// The number of required hex bytes in a prefix (i.e., a prefix must be at
103    /// least this * 2 hex chars long.)
104    // NOTE(@bpeetz): This number needs to be even. <2025-04-21>
105    pub const REQUIRED_LENGTH: usize = 2;
106
107    /// Construct an new [`IdPrefix`] from hex bytes.
108    ///
109    /// # Errors
110    /// - If the `input` does not contain ([`Self::REQUIRED_LENGTH`] * 2) hex bytes.
111    /// - If the `input` is longer than 64 hex chars.
112    ///
113    /// This is probably not what you want.
114    /// Use [`Id::shorten`][`super::Id::shorten`] to turn an [`Id`][`super::Id`]
115    /// to it's short form instead.
116    pub fn from_hex_bytes(input: &[u8]) -> Result<Self, decode::Error> {
117        if input.len() > 64 || input.len() < Self::REQUIRED_LENGTH * 2 {
118            return Err(decode::Error::InvalidLen(input.len()));
119        }
120
121        let first = {
122            let mut first_raw = [0; Self::REQUIRED_LENGTH * 2];
123            first_raw.copy_from_slice(&input[..Self::REQUIRED_LENGTH * 2]);
124
125            let mut dst = [0; Self::REQUIRED_LENGTH];
126            faster_hex::hex_decode(&first_raw, &mut dst).map_err(|err| match err {
127                faster_hex::Error::InvalidChar => decode::Error::InvalidChar,
128                faster_hex::Error::InvalidLength(_) | faster_hex::Error::Overflow => {
129                    unreachable!("We checked our numbers")
130                }
131            })?;
132            dst
133        };
134
135        let mut uneven_offset: Option<u8> = None;
136
137        let rest = {
138            let mut rest_raw = [0; 64 - (Self::REQUIRED_LENGTH * 2)];
139
140            for (slot, source) in rest_raw.iter_mut().zip(&input[Self::REQUIRED_LENGTH * 2..]) {
141                *slot = *source;
142            }
143
144            let populated_rest = input[Self::REQUIRED_LENGTH * 2..].len();
145            if populated_rest == 0 {
146                [None; 32 - Self::REQUIRED_LENGTH]
147            } else {
148                let mut trimmed_rest = &rest_raw[..populated_rest];
149
150                if (trimmed_rest.len() & 1) == 1 {
151                    // We have uneven input.
152                    // Make the trimmed_rest even, and store the dangling byte separately.
153                    uneven_offset = Some({
154                        let base = trimmed_rest[trimmed_rest.len() - 1];
155
156                        decode_hex(base)?
157                    });
158
159                    trimmed_rest = &trimmed_rest[..(trimmed_rest.len() - 1)];
160                }
161
162                let mut dst = [0; 32 - (Self::REQUIRED_LENGTH)];
163                faster_hex::hex_decode(trimmed_rest, &mut dst[..trimmed_rest.len() / 2]).map_err(
164                    |err| match err {
165                        faster_hex::Error::InvalidChar => decode::Error::InvalidChar,
166                        err @ (faster_hex::Error::InvalidLength(_)
167                        | faster_hex::Error::Overflow) => {
168                            unreachable!("We checked our numbers: {err}")
169                        }
170                    },
171                )?;
172
173                let set_rest = &dst[..trimmed_rest.len() / 2];
174
175                let mut base = [None; 32 - Self::REQUIRED_LENGTH];
176                for (slot, byte) in base.iter_mut().zip(set_rest.iter()) {
177                    *slot = Some(*byte);
178                }
179
180                base
181            }
182        };
183
184        Ok(Self {
185            first,
186            rest,
187            uneven_offset,
188        })
189    }
190
191    /// Try to resolve this prefix.
192    ///
193    /// Currently, this lists all ids of the specified [`Entity`] and tries to
194    /// find exactly on with this prefix.
195    ///
196    /// # Errors
197    /// - If it cannot read all the [`Entity's`][`Entity`] IDs.
198    /// - If more that one matches.
199    /// - If none match.
200    pub fn resolve<E: Entity + EntityRead>(
201        self,
202        repo: &Repository,
203    ) -> Result<EntityId<E>, resolve::Error> {
204        let matching = EntityIdIter::<E>::new(repo)?
205            .filter_map(|maybe_id| {
206                if let Ok(id) = maybe_id {
207                    if self.is_prefix_of(id.as_id()) {
208                        Some(Ok(id))
209                    } else {
210                        None
211                    }
212                } else {
213                    Some(maybe_id)
214                }
215            })
216            .collect::<Result<Vec<_>, _>>()?;
217
218        match matching.len().cmp(&1) {
219            std::cmp::Ordering::Less => Err(resolve::Error::NotFound),
220            std::cmp::Ordering::Equal => Ok(matching[0]),
221            std::cmp::Ordering::Greater => Err(resolve::Error::TooManyFound(matching.len())),
222        }
223    }
224
225    /// Check if this prefix is a prefix of the [`Id`] `id`.
226    #[must_use]
227    // Only asserts
228    #[allow(clippy::missing_panics_doc)]
229    pub fn is_prefix_of(&self, id: Id) -> bool {
230        let mut output = true;
231
232        if self.first == id.sha256_value[..Self::REQUIRED_LENGTH] {
233            for ((prefix, is_uneven_offset), id) in self.rest[..]
234                .iter()
235                .filter_map(|a| a.map(|b| (b, false)))
236                .chain(self.uneven_offset.iter().map(|a| (*a, true)))
237                .zip(id.sha256_value[Self::REQUIRED_LENGTH..].iter())
238            {
239                // Handle the uneven offset in a special way, as the `pr_a` value will always
240                // be invalid.
241                if is_uneven_offset {
242                    let pr_a = (prefix >> 4) & 0xF;
243                    assert_eq!(pr_a, 0);
244
245                    let pr_b = prefix & 0xF;
246
247                    // We actually, compare our pr_b with id_a, as faster hex first uses the top
248                    // part to store the u4 and only than the lower part.
249                    let id_a = (id >> 4) & 0xF;
250
251                    output = pr_b == id_a;
252                } else {
253                    let pr_a = (prefix >> 4) & 0xF;
254                    let pr_b = prefix & 0xF;
255
256                    let id_a = (id >> 4) & 0xF;
257                    let id_b = id & 0xF;
258
259                    output = output && pr_a == id_a && pr_b == id_b;
260                }
261            }
262
263            output
264        } else {
265            false
266        }
267    }
268}
269
270#[allow(missing_docs)]
271pub mod resolve {
272    #[derive(Debug, thiserror::Error)]
273    /// The Error returned from
274    /// [`IdPrefix::resolve`][`super::IdPrefix::resolve`].
275    pub enum Error {
276        #[error("Failed to resolve this id, because an id de-referencing operation failed: {0}")]
277        FailedGet(#[from] crate::replica::get::Error),
278
279        #[error("Failed to resolve prefix, because we did not find a matching id.")]
280        NotFound,
281
282        #[error("Failed to resolve prefix, because we found too many matching ids ({0})")]
283        TooManyFound(usize),
284    }
285}
286
287#[allow(missing_docs)]
288pub mod decode {
289    use std::str::FromStr;
290
291    use crate::replica::entity::id::prefix::IdPrefix;
292
293    #[derive(Debug, thiserror::Error, Clone, Copy)]
294    /// The Error returned from [`IdPrefix::from_hex_bytes`].
295    pub enum Error {
296        #[error(
297            "Your prefix input was {0} bytes long, but should be at least {len} bytes and \
298                not more than 64.",
299            len = IdPrefix::REQUIRED_LENGTH * 2
300        )]
301        InvalidLen(usize),
302
303        #[error(
304            "Your prefix input was not some for the required {first} bytes",
305            first = IdPrefix::REQUIRED_LENGTH * 2
306        )]
307        FirstNotSome,
308
309        #[error("Your hex bytes contained an invalid hex char.")]
310        InvalidChar,
311    }
312
313    impl FromStr for IdPrefix {
314        type Err = Error;
315
316        fn from_str(s: &str) -> Result<Self, Self::Err> {
317            Self::from_hex_bytes(s.as_bytes())
318        }
319    }
320
321    impl TryFrom<&str> for IdPrefix {
322        type Error = Error;
323
324        fn try_from(value: &str) -> Result<Self, Self::Error> {
325            <Self as FromStr>::from_str(value)
326        }
327    }
328}
329
330fn decode_hex(base: u8) -> Result<u8, decode::Error> {
331    let val = match base {
332        b'0'..=b'9' => base - b'0',
333        b'a'..=b'f' => base - b'a' + 10,
334        b'A'..=b'F' => base - b'A' + 10,
335        _ => return Err(decode::Error::InvalidChar),
336    };
337    Ok(val)
338}
339fn encode_hex(base: u8) -> char {
340    match base {
341        0..=9 => (b'0' + base) as char,
342        10..=15 => (b'a' + (base - 10)) as char,
343        _ => unreachable!(),
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use std::str::FromStr;
350
351    use super::IdPrefix;
352    use crate::replica::entity::id::{
353        Id,
354        prefix::{decode_hex, encode_hex},
355    };
356
357    const HEX_CHARS: [(u8, char); 16] = [
358        (0, '0'),
359        (1, '1'),
360        (2, '2'),
361        (3, '3'),
362        (4, '4'),
363        (5, '5'),
364        (6, '6'),
365        (7, '7'),
366        (8, '8'),
367        (9, '9'),
368        (10, 'a'),
369        (11, 'b'),
370        (12, 'c'),
371        (13, 'd'),
372        (14, 'e'),
373        (15, 'f'),
374    ];
375
376    #[test]
377    fn test_print() {
378        let id = Id::from_hex(b"e30589c8275f1cafd4485f1d414a9c2da0d9cb9b4122f37aa2f557d988e1f87f")
379            .unwrap();
380        let prefix = id.shorten();
381
382        assert_eq!(prefix.to_string().as_str(), "e30589c");
383    }
384
385    #[test]
386    fn test_print_2() {
387        let id = Id::from_hex(b"b8f4a62333974e95eb69e6956d07e799662acefd28b00ab83fcd72d1ef6522eb")
388            .unwrap();
389
390        let prefix = id.shorten();
391        assert_eq!(prefix.to_string().as_str(), "b8f4a62");
392    }
393
394    #[test]
395    fn test_prefix_of_wrong() {
396        let id = Id::from_hex(b"a7c3efc6b86b96023641e49b2ec96a4d54406fc5f164d7e79d2bdf66170de892")
397            .unwrap();
398        let prefix = IdPrefix::from_str("a7c3f").unwrap();
399
400        assert!(!prefix.is_prefix_of(id));
401    }
402
403    #[test]
404    fn test_prefix_of() {
405        let id_str = "fe5ae5b4657a04784c6306473d0e0ec692b40a993195ab16a0e5019d19fbc01c";
406        let id = Id::from_hex(id_str.as_bytes()).unwrap();
407
408        for len in (IdPrefix::REQUIRED_LENGTH * 2)..(id_str.len()) {
409            let prefix_str = &id_str[..len];
410
411            eprintln!("Testing prefix with len: {len} ({prefix_str})");
412
413            let prefix = IdPrefix::from_str(prefix_str).unwrap();
414            assert!(prefix.is_prefix_of(id));
415        }
416    }
417
418    #[test]
419    fn decode_0() {
420        assert_eq!(decode_hex(b'0').unwrap(), 0);
421    }
422    #[test]
423    fn decode_a() {
424        assert_eq!(decode_hex(b'a').unwrap(), 10);
425    }
426    #[test]
427    #[allow(non_snake_case)]
428    fn decode_F() {
429        assert_eq!(decode_hex(b'F').unwrap(), 15);
430    }
431
432    #[test]
433    fn test_all() {
434        for (value, name) in HEX_CHARS {
435            assert_eq!(value, decode_hex(name as u8).unwrap());
436            assert_eq!(encode_hex(value), name);
437        }
438    }
439
440    #[test]
441    fn encode_0() {
442        assert_eq!(encode_hex(0), '0');
443    }
444    #[test]
445    fn encode_a() {
446        assert_eq!(encode_hex(10), 'a');
447    }
448    #[test]
449    fn encode_f() {
450        assert_eq!(encode_hex(15), 'f');
451    }
452}