Skip to main content

gix_hash/
prefix.rs

1use std::cmp::Ordering;
2
3use crate::{ChangeId, ObjectId, Prefix, change_id::ReverseHexDisplay, oid};
4
5/// The error returned by [`Prefix::new()`].
6#[derive(Debug, thiserror::Error)]
7#[expect(missing_docs)]
8pub enum Error {
9    #[error(
10        "The minimum hex length of a short object id is {}, got {hex_len}",
11        Prefix::MIN_HEX_LEN
12    )]
13    TooShort { hex_len: usize },
14    #[error("An object of kind {object_kind} cannot be larger than {} in hex, but {hex_len} was requested", object_kind.len_in_hex())]
15    TooLong { object_kind: crate::Kind, hex_len: usize },
16}
17
18///
19pub mod from_hex {
20    /// The error returned by [`Prefix::from_hex`][super::Prefix::from_hex()].
21    #[derive(Debug, Eq, PartialEq, thiserror::Error)]
22    #[expect(missing_docs)]
23    pub enum Error {
24        #[error(
25            "The minimum hex length of a short object id is {}, got {hex_len}",
26            super::Prefix::MIN_HEX_LEN
27        )]
28        TooShort { hex_len: usize },
29        #[error("An id cannot be larger than {} chars in hex, but {hex_len} was requested", crate::Kind::longest().len_in_hex())]
30        TooLong { hex_len: usize },
31        #[error("Invalid hex character")]
32        Invalid,
33    }
34}
35
36impl Prefix {
37    /// The smallest allowed prefix length below which chances for collisions are too high even in small repositories.
38    pub const MIN_HEX_LEN: usize = 4;
39
40    /// Create a new instance by taking a full `id` as input and truncating it to `hex_len`.
41    ///
42    /// For instance, with `hex_len` of 7 the resulting prefix is 3.5 bytes, or 3 bytes and 4 bits
43    /// wide, with all other bytes and bits set to zero.
44    pub fn new(id: &oid, hex_len: usize) -> Result<Self, Error> {
45        if hex_len > id.kind().len_in_hex() {
46            Err(Error::TooLong {
47                object_kind: id.kind(),
48                hex_len,
49            })
50        } else if hex_len < Self::MIN_HEX_LEN {
51            Err(Error::TooShort { hex_len })
52        } else {
53            let mut prefix = ObjectId::null(id.kind());
54            let b = prefix.as_mut_slice();
55            let copy_len = hex_len.div_ceil(2);
56            b[..copy_len].copy_from_slice(&id.as_bytes()[..copy_len]);
57            if hex_len % 2 == 1 {
58                b[hex_len / 2] &= 0xf0;
59            }
60
61            Ok(Prefix { bytes: prefix, hex_len })
62        }
63    }
64
65    /// Write this prefix into `buf` as lowercase hexadecimal characters and return the initialized portion.
66    ///
67    /// # Panics
68    ///
69    /// If `buf` is shorter than [`Self::hex_len()`].
70    #[inline]
71    #[must_use]
72    pub fn hex_to_buf<'a>(&self, buf: &'a mut [u8]) -> &'a mut str {
73        let complete_bytes = self.hex_len / 2;
74        let complete_hex_len = complete_bytes * 2;
75        if complete_bytes != 0 {
76            faster_hex::hex_encode(&self.bytes.as_bytes()[..complete_bytes], &mut buf[..complete_hex_len])
77                .expect("buffer size was checked before encoding");
78        }
79        if self.hex_len % 2 == 1 {
80            const HEX: &[u8; 16] = b"0123456789abcdef";
81            buf[complete_hex_len] = HEX[usize::from(self.bytes.as_bytes()[complete_bytes] >> 4)];
82        }
83        std::str::from_utf8_mut(&mut buf[..self.hex_len]).expect("hexadecimal object IDs are valid UTF-8")
84    }
85
86    /// Write this prefix to `out` as lowercase hexadecimal characters.
87    #[inline]
88    pub fn write_hex_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> {
89        let mut buf = crate::Kind::hex_buf();
90        out.write_all(self.hex_to_buf(&mut buf).as_bytes())
91    }
92
93    /// Returns the prefix as object id.
94    ///
95    /// Note that it may be deceptive to use given that it looks like a full
96    /// object id, even though its post-prefix bytes/bits are set to zero.
97    pub fn as_oid(&self) -> &oid {
98        &self.bytes
99    }
100
101    /// Return the amount of hexadecimal characters that are set in the prefix.
102    ///
103    /// This gives the prefix a granularity of 4 bits.
104    pub fn hex_len(&self) -> usize {
105        self.hex_len
106    }
107
108    /// Provided with candidate id which is a full hash, determine how this prefix compares to it,
109    /// only looking at the prefix bytes, ignoring everything behind that.
110    pub fn cmp_oid(&self, candidate: &oid) -> Ordering {
111        let common_len = self.hex_len / 2;
112
113        self.bytes.as_bytes()[..common_len]
114            .cmp(&candidate.as_bytes()[..common_len])
115            .then(if self.hex_len % 2 == 1 {
116                let half_byte_idx = self.hex_len / 2;
117                self.bytes.as_bytes()[half_byte_idx].cmp(&(candidate.as_bytes()[half_byte_idx] & 0xf0))
118            } else {
119                Ordering::Equal
120            })
121    }
122
123    /// Create an instance from the given hexadecimal prefix `value`, e.g. `35e77c16` would yield a `Prefix` with `hex_len()` = 8.
124    /// Note that the minimum hex length is `4` - use [`Self::from_hex_nonempty()`].
125    pub fn from_hex(value: &str) -> Result<Self, from_hex::Error> {
126        let hex_len = value.len();
127        if hex_len < Self::MIN_HEX_LEN {
128            return Err(from_hex::Error::TooShort { hex_len });
129        }
130        Self::from_hex_nonempty(value)
131    }
132
133    /// Create an instance from the given hexadecimal prefix `value`, e.g. `35e` would yield a `Prefix` with `hex_len()` = 3.
134    /// Note that this function supports all non-empty hex input - for a more typical implementation, use [`Self::from_hex()`].
135    pub fn from_hex_nonempty(value: &str) -> Result<Self, from_hex::Error> {
136        let hex_len = value.len();
137
138        if hex_len > crate::Kind::longest().len_in_hex() {
139            return Err(from_hex::Error::TooLong { hex_len });
140        } else if hex_len == 0 {
141            return Err(from_hex::Error::TooShort { hex_len });
142        }
143
144        let kind = crate::Kind::from_hex_len(hex_len).expect("hex-len is already checked");
145        let mut bytes = ObjectId::null(kind);
146        let dst = &mut bytes.as_mut_slice()[..hex_len.div_ceil(2)];
147        let decode_result = if hex_len % 2 == 0 {
148            faster_hex::hex_decode(value.as_bytes(), dst)
149        } else {
150            let mut hex = crate::Kind::hex_buf();
151            hex[..hex_len].copy_from_slice(value.as_bytes());
152            hex[hex_len] = b'0';
153            faster_hex::hex_decode(&hex[..=hex_len], dst)
154        };
155        decode_result.map_err(|e| match e {
156            faster_hex::Error::InvalidChar | faster_hex::Error::Overflow => from_hex::Error::Invalid,
157            faster_hex::Error::InvalidLength(_) => panic!("This is already checked"),
158        })?;
159
160        Ok(Prefix { bytes, hex_len })
161    }
162
163    /// Create an instance from a reverse-hex prefix, requiring at least [`Self::MIN_HEX_LEN`] characters.
164    pub fn from_reverse_hex(value: &str) -> Result<Self, from_hex::Error> {
165        let hex_len = value.len();
166        if hex_len < Self::MIN_HEX_LEN {
167            return Err(from_hex::Error::TooShort { hex_len });
168        }
169        Self::from_reverse_hex_nonempty(value)
170    }
171
172    /// Create an instance from a non-empty prefix written with JJ's reverse-hex alphabet.
173    pub fn from_reverse_hex_nonempty(value: &str) -> Result<Self, from_hex::Error> {
174        let hex_len = value.len();
175        if hex_len > crate::Kind::longest().len_in_hex() {
176            return Err(from_hex::Error::TooLong { hex_len });
177        } else if hex_len == 0 {
178            return Err(from_hex::Error::TooShort { hex_len });
179        }
180
181        let mut hex = crate::Kind::hex_buf();
182        crate::change_id::reverse_hex_to_hex(value.as_bytes(), &mut hex[..hex_len])
183            .map_err(|()| from_hex::Error::Invalid)?;
184        let hex = std::str::from_utf8(&hex[..hex_len]).expect("translated reverse hex is always ASCII");
185        Self::from_hex_nonempty(hex)
186    }
187
188    /// Return a type which displays this prefix in JJ-compatible reverse-hex notation.
189    pub fn to_reverse_hex(&self) -> ReverseHexDisplay<'_> {
190        ReverseHexDisplay::new(&self.bytes, self.hex_len)
191    }
192}
193
194/// Create an instance from the given hexadecimal prefix, e.g. `35e77c16` would yield a `Prefix`
195/// with `hex_len()` = 8.
196impl TryFrom<&str> for Prefix {
197    type Error = from_hex::Error;
198
199    fn try_from(value: &str) -> Result<Self, Self::Error> {
200        Prefix::from_hex(value)
201    }
202}
203
204impl std::fmt::Display for Prefix {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        self.bytes.to_hex_with_len(self.hex_len).fmt(f)
207    }
208}
209
210impl Prefix {
211    fn eq_str(&self, other: &str) -> bool {
212        self.bytes.to_hex_with_len(self.hex_len).eq_str(other)
213    }
214}
215
216// Keep this directional as the hash kind and unused suffix aren't uniquely identified by the displayed prefix.
217impl_partial_eq_str_one_way!(Prefix);
218
219impl From<ObjectId> for Prefix {
220    fn from(oid: ObjectId) -> Self {
221        Prefix {
222            bytes: oid,
223            hex_len: oid.kind().len_in_hex(),
224        }
225    }
226}
227
228impl From<ChangeId> for Prefix {
229    fn from(change_id: ChangeId) -> Self {
230        ObjectId::from(change_id).into()
231    }
232}