Re: [PATCH 11/14] rust: add functionality to hash an object
- From
Ezekiel Newren <ezekielnewren@gmail.com>
- Date
- Oct 28, 2025, 18:05 UTC
- Message-ID
- <CAH=ZcbDCrYuSW7nLerQZnT-R_CoCtN2RNycLqOEEV-T-T7VoZQ@mail.gmail.com>
- In-Reply-To
- <20251027004404.2152927-12-sandals@crustytoothpaste.net>
On Sun, Oct 26, 2025 at 6:44 PM brian m. carlson <sandals@crustytoothpaste.net> wrote:
Show 37 quoted lines
>
> In a future commit, we'll want to hash some data when dealing with a
> loose object map. Let's make this easy by creating a structure to hash
> objects and calling into the C functions as necessary to perform the
> hashing. For now, we only implement safe hashing, but in the future we
> could add unsafe hashing if we want. Implement Clone and Drop to
> appropriately manage our memory. Additionally implement Write to make
> it easy to use with other formats that implement this trait.
>
> While we're at it, add some tests for the various cases in this file.
>
> Signed-off-by: brian m. carlson <sandals@crustytoothpaste.net>
> ---
> src/hash.rs | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 157 insertions(+)
>
> diff --git a/src/hash.rs b/src/hash.rs
> index a5b9493bd8..8798a50aef 100644
> --- a/src/hash.rs
> +++ b/src/hash.rs
> @@ -10,6 +10,7 @@
> // You should have received a copy of the GNU General Public License along
> // with this program; if not, see <https://www.gnu.org/licenses/>.
>
> +use std::io::{self, Write};
> use std::os::raw::c_void;
>
> pub const GIT_MAX_RAWSZ: usize = 32;
> @@ -39,6 +40,81 @@ impl ObjectID {
> }
> }
>
> +pub struct Hasher {
> + algo: HashAlgorithm,
> + safe: bool,
> + ctx: *mut c_void,
> +}The name _Hasher_ is already used by std::hash::Hasher. It would be preferable to pick a different name to avoid confusion. Perhaps CryptoHasher, SecureHasher?
Show 11 quoted lines
> +impl Hasher {
> + /// Create a new safe hasher.
> + pub fn new(algo: HashAlgorithm) -> Hasher {
> + let ctx = unsafe { c::git_hash_alloc() };
> + unsafe { c::git_hash_init(ctx, algo.hash_algo_ptr()) };
> + Hasher {
> + algo,
> + safe: true,
> + ctx,
> + }
> + }- pub fn new(algo: HashAlgorithm) -> Hasher {
+ pub fn new(algo: HashAlgorithm) -> Self {
let ctx = unsafe { c::git_hash_alloc() };
unsafe { c::git_hash_init(ctx, algo.hash_algo_ptr()) };
- Hasher {
+ Self {
algo,
safe: true,
ctx,
}> + /// Return whether this is a safe hasher.
> + pub fn is_safe(&self) -> bool {
> + self.safe
> + }I don't understand the point in being able to query whether a given hasher is safe or not. How does that change how this hasher code is used? If the functions are safe then you wouldn't wrap it in an unsafe block. If the functions are declared with unsafe then you'd always need to wrap it in an unsafe block whether it's actually safe or not. Using unsafe in Rust isn't like error handling where you do something different on failure. If something fails in unsafe it's usually unrecoverable e.g. segfault due to invalid memory access. My understanding of unsafe in Rust means "The compiler can't verify that this code is actually safe to run, so I've made sure that it is safe myself and I'll let the compiler know what code to ignore during compilation."
Show 51 quoted lines
> + /// Update the hasher with the specified data.
> + pub fn update(&mut self, data: &[u8]) {
> + unsafe { c::git_hash_update(self.ctx, data.as_ptr() as *const c_void, data.len()) };
> + }
> +
> + /// Return an object ID, consuming the hasher.
> + pub fn into_oid(self) -> ObjectID {
> + let mut oid = ObjectID {
> + hash: [0u8; 32],
> + algo: self.algo as u32,
> + };
> + unsafe { c::git_hash_final_oid(&mut oid as *mut ObjectID as *mut c_void, self.ctx) };
> + oid
> + }
> +
> + /// Return a hash as a `Vec`, consuming the hasher.
> + pub fn into_vec(self) -> Vec<u8> {
> + let mut v = vec![0u8; self.algo.raw_len()];
> + unsafe { c::git_hash_final(v.as_mut_ptr(), self.ctx) };
> + v
> + }
> +}
> +
> +impl Write for Hasher {
> + fn write(&mut self, data: &[u8]) -> io::Result<usize> {
> + self.update(data);
> + Ok(data.len())
> + }
> +
> + fn flush(&mut self) -> io::Result<()> {
> + Ok(())
> + }
> +}
> +
> +impl Clone for Hasher {
> + fn clone(&self) -> Hasher {
> + let ctx = unsafe { c::git_hash_alloc() };
> + unsafe { c::git_hash_clone(ctx, self.ctx) };
> + Hasher {
> + algo: self.algo,
> + safe: self.safe,
> + ctx,
> + }
> + }
> +}
> +
> +impl Drop for Hasher {
> + fn drop(&mut self) {
> + unsafe { c::git_hash_free(self.ctx) };
> + }
> +}Make sense.
Show 97 quoted lines
> /// A hash algorithm,
> #[repr(C)]
> #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
> @@ -167,6 +243,11 @@ impl HashAlgorithm {
> pub fn hash_algo_ptr(self) -> *const c_void {
> unsafe { c::hash_algo_ptr_by_offset(self as u32) }
> }
> +
> + /// Create a hasher for this algorithm.
> + pub fn hasher(self) -> Hasher {
> + Hasher::new(self)
> + }
> }
>
> pub mod c {
> @@ -174,5 +255,81 @@ pub mod c {
>
> extern "C" {
> pub fn hash_algo_ptr_by_offset(n: u32) -> *const c_void;
> + pub fn unsafe_hash_algo(algop: *const c_void) -> *const c_void;
> + pub fn git_hash_alloc() -> *mut c_void;
> + pub fn git_hash_free(ctx: *mut c_void);
> + pub fn git_hash_init(dst: *mut c_void, algop: *const c_void);
> + pub fn git_hash_clone(dst: *mut c_void, src: *const c_void);
> + pub fn git_hash_update(ctx: *mut c_void, inp: *const c_void, len: usize);
> + pub fn git_hash_final(hash: *mut u8, ctx: *mut c_void);
> + pub fn git_hash_final_oid(hash: *mut c_void, ctx: *mut c_void);
> + }
> +}
> +
> +#[cfg(test)]
> +mod tests {
> + use super::{HashAlgorithm, ObjectID};
> + use std::io::Write;
> +
> + fn all_algos() -> &'static [HashAlgorithm] {
> + &[HashAlgorithm::SHA1, HashAlgorithm::SHA256]
> + }
> +
> + #[test]
> + fn format_id_round_trips() {
> + for algo in all_algos() {
> + assert_eq!(
> + *algo,
> + HashAlgorithm::from_format_id(algo.format_id()).unwrap()
> + );
> + }
> + }
> +
> + #[test]
> + fn offset_round_trips() {
> + for algo in all_algos() {
> + assert_eq!(*algo, HashAlgorithm::from_u32(*algo as u32).unwrap());
> + }
> + }
> +
> + #[test]
> + fn slices_have_correct_length() {
> + for algo in all_algos() {
> + for oid in [algo.null_oid(), algo.empty_blob(), algo.empty_tree()] {
> + assert_eq!(oid.as_slice().len(), algo.raw_len());
> + }
> + }
> + }
> +
> + #[test]
> + fn hasher_works_correctly() {
> + for algo in all_algos() {
> + let tests: &[(&[u8], &ObjectID)] = &[
> + (b"blob 0\0", algo.empty_blob()),
> + (b"tree 0\0", algo.empty_tree()),
> + ];
> + for (data, oid) in tests {
> + let mut h = algo.hasher();
> + assert_eq!(h.is_safe(), true);
> + // Test that this works incrementally.
> + h.update(&data[0..2]);
> + h.update(&data[2..]);
> +
> + let h2 = h.clone();
> +
> + let actual_oid = h.into_oid();
> + assert_eq!(**oid, actual_oid);
> +
> + let v = h2.into_vec();
> + assert_eq!((*oid).as_slice(), &v);
> +
> + let mut h = algo.hasher();
> + h.write_all(&data[0..2]).unwrap();
> + h.write_all(&data[2..]).unwrap();
> +
> + let actual_oid = h.into_oid();
> + assert_eq!(**oid, actual_oid);
> + }
> + }
> }
> }Looks good.