From e2a1e4dff5f1aad2c630f04698f2c2ee593772ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Wed, 10 Dec 2025 20:25:13 +0100 Subject: [PATCH 1/7] Add `ObjectId::Sha256` and `Kind::Sha256` --- gix-hash/src/hasher.rs | 1 + gix-hash/src/io.rs | 4 ++++ gix-hash/src/kind.rs | 6 ++++++ gix-hash/src/lib.rs | 5 +++++ gix-hash/src/object_id.rs | 41 ++++++++++++++++++++++++++++++++++----- gix-hash/src/oid.rs | 31 ++++++++++++++++++++++++++--- 6 files changed, 80 insertions(+), 8 deletions(-) diff --git a/gix-hash/src/hasher.rs b/gix-hash/src/hasher.rs index 4519f599e45..a214c0ca7f3 100644 --- a/gix-hash/src/hasher.rs +++ b/gix-hash/src/hasher.rs @@ -67,6 +67,7 @@ pub(super) mod _impl { pub fn hasher(kind: crate::Kind) -> Hasher { match kind { crate::Kind::Sha1 => Hasher::default(), + crate::Kind::Sha256 => Hasher::default(), } } } diff --git a/gix-hash/src/io.rs b/gix-hash/src/io.rs index 19e981453dc..f2c91890265 100644 --- a/gix-hash/src/io.rs +++ b/gix-hash/src/io.rs @@ -118,6 +118,10 @@ pub(super) mod _impl { inner, hash: crate::hasher(object_hash), }, + crate::Kind::Sha256 => Write { + inner, + hash: crate::hasher(object_hash), + }, } } } diff --git a/gix-hash/src/kind.rs b/gix-hash/src/kind.rs index de787b417fd..8a619521ad7 100644 --- a/gix-hash/src/kind.rs +++ b/gix-hash/src/kind.rs @@ -28,6 +28,7 @@ impl std::fmt::Display for Kind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Kind::Sha1 => f.write_str("SHA1"), + Kind::Sha256 => f.write_str("SHA256"), } } } @@ -62,13 +63,16 @@ impl Kind { pub const fn len_in_hex(&self) -> usize { match self { Kind::Sha1 => 40, + Kind::Sha256 => 64, } } + /// Returns the amount of bytes taken up by the hash of this instance. #[inline] pub const fn len_in_bytes(&self) -> usize { match self { Kind::Sha1 => 20, + Kind::Sha256 => 32, } } @@ -103,6 +107,7 @@ impl Kind { pub fn null_ref(&self) -> &'static oid { match self { Kind::Sha1 => oid::null_sha1(), + Kind::Sha256 => oid::null_sha256(), } } @@ -111,6 +116,7 @@ impl Kind { pub const fn null(&self) -> ObjectId { match self { Kind::Sha1 => ObjectId::null_sha1(), + Kind::Sha256 => ObjectId::null_sha256(), } } diff --git a/gix-hash/src/lib.rs b/gix-hash/src/lib.rs index 02e7bc2420c..c2514f6af3d 100644 --- a/gix-hash/src/lib.rs +++ b/gix-hash/src/lib.rs @@ -49,6 +49,9 @@ pub struct Prefix { /// The size of a SHA1 hash digest in bytes. const SIZE_OF_SHA1_DIGEST: usize = 20; +/// The size of a SHA256 hash digest in bytes. +const SIZE_OF_SHA256_DIGEST: usize = 32; + /// Denotes the kind of function to produce a [`ObjectId`]. #[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -57,6 +60,8 @@ pub enum Kind { /// The Sha1 hash with 160 bits. #[default] Sha1 = 1, + /// The Sha256 hash with 256 bits. + Sha256 = 2, } mod kind; diff --git a/gix-hash/src/object_id.rs b/gix-hash/src/object_id.rs index a2deb178294..9d4c1b91775 100644 --- a/gix-hash/src/object_id.rs +++ b/gix-hash/src/object_id.rs @@ -4,7 +4,7 @@ use std::{ ops::Deref, }; -use crate::{borrowed::oid, Kind, SIZE_OF_SHA1_DIGEST}; +use crate::{borrowed::oid, Kind, SIZE_OF_SHA1_DIGEST, SIZE_OF_SHA256_DIGEST}; /// An owned hash identifying objects, most commonly `Sha1` #[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Copy)] @@ -13,6 +13,8 @@ use crate::{borrowed::oid, Kind, SIZE_OF_SHA1_DIGEST}; pub enum ObjectId { /// A SHA 1 hash digest Sha1([u8; SIZE_OF_SHA1_DIGEST]), + /// A SHA 256 hash digest + Sha256([u8; SIZE_OF_SHA256_DIGEST]), } // False positive: https://github.com/rust-lang/rust-clippy/issues/2627 @@ -84,6 +86,7 @@ impl ObjectId { pub fn kind(&self) -> Kind { match self { ObjectId::Sha1(_) => Kind::Sha1, + ObjectId::Sha256(_) => Kind::Sha256, } } /// Return the raw byte slice representing this hash. @@ -91,6 +94,7 @@ impl ObjectId { pub fn as_slice(&self) -> &[u8] { match self { Self::Sha1(b) => b.as_ref(), + Self::Sha256(b) => b.as_ref(), } } /// Return the raw mutable byte slice representing this hash. @@ -98,6 +102,7 @@ impl ObjectId { pub fn as_mut_slice(&mut self) -> &mut [u8] { match self { Self::Sha1(b) => b.as_mut(), + Self::Sha256(b) => b.as_mut(), } } @@ -108,6 +113,9 @@ impl ObjectId { Kind::Sha1 => { ObjectId::Sha1(*b"\xe6\x9d\xe2\x9b\xb2\xd1\xd6\x43\x4b\x8b\x29\xae\x77\x5a\xd8\xc2\xe4\x8c\x53\x91") } + Kind::Sha256 => { + ObjectId::Sha256(*b"\x47\x3a\x0f\x4c\x3b\xe8\xa9\x36\x81\xa2\x67\xe3\xb1\xe9\xa7\xdc\xda\x11\x85\x43\x6f\xe1\x41\xf7\x74\x91\x20\xa3\x03\x72\x18\x13") + } } } @@ -118,6 +126,9 @@ impl ObjectId { Kind::Sha1 => { ObjectId::Sha1(*b"\x4b\x82\x5d\xc6\x42\xcb\x6e\xb9\xa0\x60\xe5\x4b\xf8\xd6\x92\x88\xfb\xee\x49\x04") } + Kind::Sha256 => { + ObjectId::Sha256(*b"\x6e\xf1\x9b\x41\x22\x5c\x53\x69\xf1\xc1\x04\xd4\x5d\x8d\x85\xef\xa9\xb0\x57\xb5\x3b\x14\xb4\xb9\xb9\x39\xdd\x74\xde\xcc\x53\x21") + }, } } @@ -127,6 +138,7 @@ impl ObjectId { pub const fn null(kind: Kind) -> ObjectId { match kind { Kind::Sha1 => Self::null_sha1(), + Kind::Sha256 => Self::null_sha256(), } } @@ -136,6 +148,7 @@ impl ObjectId { pub fn is_null(&self) -> bool { match self { ObjectId::Sha1(digest) => &digest[..] == oid::null_sha1().as_bytes(), + ObjectId::Sha256(digest) => &digest[..] == oid::null_sha256().as_bytes(), } } @@ -167,15 +180,15 @@ impl ObjectId { /// Sha1 hash specific methods impl ObjectId { - /// Instantiate an Digest from 20 bytes of a Sha1 digest. + /// Instantiate a Digest from 20 bytes of a Sha1 digest. #[inline] fn new_sha1(id: [u8; SIZE_OF_SHA1_DIGEST]) -> Self { ObjectId::Sha1(id) } - /// Instantiate an Digest from a slice 20 borrowed bytes of a Sha1 digest. + /// Instantiate a Digest from a slice 20 borrowed bytes of a Sha1 digest. /// - /// Panics of the slice doesn't have a length of 20. + /// Panics if the slice doesn't have a length of 20. #[inline] pub(crate) fn from_20_bytes(b: &[u8]) -> ObjectId { let mut id = [0; SIZE_OF_SHA1_DIGEST]; @@ -183,17 +196,34 @@ impl ObjectId { ObjectId::Sha1(id) } - /// Returns an Digest representing a Sha1 with whose memory is zeroed. + /// Instantiate a Digest from a slice 32 borrowed bytes of a Sha256 digest. + /// + /// Panics if the slice doesn't have a length of 32. + #[inline] + pub(crate) fn from_32_bytes(b: &[u8]) -> ObjectId { + let mut id = [0; SIZE_OF_SHA256_DIGEST]; + id.copy_from_slice(b); + ObjectId::Sha256(id) + } + + /// Returns a Digest representing a Sha1 whose memory is zeroed. #[inline] pub(crate) const fn null_sha1() -> ObjectId { ObjectId::Sha1([0u8; 20]) } + + /// Returns a Digest representing a Sha256 whose memory is zeroed. + #[inline] + pub(crate) const fn null_sha256() -> ObjectId { + ObjectId::Sha256([0u8; SIZE_OF_SHA256_DIGEST]) + } } impl std::fmt::Debug for ObjectId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ObjectId::Sha1(_hash) => f.write_str("Sha1(")?, + ObjectId::Sha256(_) => f.write_str("Sha256(")?, } for b in self.as_bytes() { write!(f, "{b:02x}")?; @@ -212,6 +242,7 @@ impl From<&oid> for ObjectId { fn from(v: &oid) -> Self { match v.kind() { Kind::Sha1 => ObjectId::from_20_bytes(v.as_bytes()), + Kind::Sha256 => ObjectId::from_32_bytes(v.as_bytes()), } } } diff --git a/gix-hash/src/oid.rs b/gix-hash/src/oid.rs index 0d952b219b0..89484f791c7 100644 --- a/gix-hash/src/oid.rs +++ b/gix-hash/src/oid.rs @@ -1,6 +1,6 @@ use std::hash; -use crate::{Kind, ObjectId, SIZE_OF_SHA1_DIGEST}; +use crate::{Kind, ObjectId, SIZE_OF_SHA1_DIGEST, SIZE_OF_SHA256_DIGEST}; /// A borrowed reference to a hash identifying objects. /// @@ -57,6 +57,7 @@ impl std::fmt::Debug for oid { "{}({})", match self.kind() { Kind::Sha1 => "Sha1", + Kind::Sha256 => "Sha256", }, self.to_hex(), ) @@ -146,6 +147,7 @@ impl oid { pub fn is_null(&self) -> bool { match self.kind() { Kind::Sha1 => &self.bytes == oid::null_sha1().as_bytes(), + Kind::Sha256 => &self.bytes == oid::null_sha256().as_bytes(), } } @@ -154,6 +156,7 @@ impl oid { pub fn is_empty_blob(&self) -> bool { match self.kind() { Kind::Sha1 => &self.bytes == oid::empty_blob_sha1().as_bytes(), + Kind::Sha256 => &self.bytes == oid::empty_blob_sha256().as_bytes(), } } @@ -162,6 +165,7 @@ impl oid { pub fn is_empty_tree(&self) -> bool { match self.kind() { Kind::Sha1 => &self.bytes == oid::empty_tree_sha1().as_bytes(), + Kind::Sha256 => &self.bytes == oid::empty_tree_sha256().as_bytes(), } } } @@ -192,17 +196,37 @@ impl oid { oid::from_bytes([0u8; SIZE_OF_SHA1_DIGEST].as_ref()) } - /// Returns an oid representing the hash of an empty blob. + /// Returns a Sha256 digest with all bytes being initialized to zero. + #[inline] + pub(crate) fn null_sha256() -> &'static Self { + oid::from_bytes([0u8; SIZE_OF_SHA256_DIGEST].as_ref()) + } + + /// Returns an oid representing the SHA-1 hash of an empty blob. #[inline] pub(crate) fn empty_blob_sha1() -> &'static Self { oid::from_bytes(b"\xe6\x9d\xe2\x9b\xb2\xd1\xd6\x43\x4b\x8b\x29\xae\x77\x5a\xd8\xc2\xe4\x8c\x53\x91") } - /// Returns an oid representing the hash of an empty tree. + /// Returns an oid representing the SHA-256 hash of an empty blob. + #[inline] + pub(crate) fn empty_blob_sha256() -> &'static Self { + oid::from_bytes(b"\x47\x3a\x0f\x4c\x3b\xe8\xa9\x36\x81\xa2\x67\xe3\xb1\xe9\xa7\xdc\xda\x11\x85\x43\x6f\xe1\x41\xf7\x74\x91\x20\xa3\x03\x72\x18\x13") + } + + /// Returns an oid representing the SHA-1 hash of an empty tree. #[inline] pub(crate) fn empty_tree_sha1() -> &'static Self { oid::from_bytes(b"\x4b\x82\x5d\xc6\x42\xcb\x6e\xb9\xa0\x60\xe5\x4b\xf8\xd6\x92\x88\xfb\xee\x49\x04") } + + /// Returns an oid representing the SHA-256 hash of an empty tree. + #[inline] + pub(crate) fn empty_tree_sha256() -> &'static Self { + oid::from_bytes( + b"\x6e\xf1\x9b\x41\x22\x5c\x53\x69\xf1\xc1\x04\xd4\x5d\x8d\x85\xef\xa9\xb0\x57\xb5\x3b\x14\xb4\xb9\xb9\x39\xdd\x74\xde\xcc\x53\x21", + ) + } } impl AsRef for &oid { @@ -225,6 +249,7 @@ impl ToOwned for oid { fn to_owned(&self) -> Self::Owned { match self.kind() { Kind::Sha1 => ObjectId::Sha1(self.bytes.try_into().expect("no bug in hash detection")), + Kind::Sha256 => ObjectId::Sha256(self.bytes.try_into().expect("no bug in hash detection")), } } } From 8afb22eec33ea96299a423f4ce62a060904b5bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Fri, 12 Dec 2025 08:01:00 +0100 Subject: [PATCH 2/7] Introduce feature flag `sha256` --- gix-hash/Cargo.toml | 2 ++ gix-hash/src/hasher.rs | 1 + gix-hash/src/io.rs | 1 + gix-hash/src/kind.rs | 31 +++++++++++++++++++++++++++++-- gix-hash/src/lib.rs | 2 ++ gix-hash/src/object_id.rs | 19 ++++++++++++++++++- gix-hash/src/oid.rs | 13 ++++++++++++- 7 files changed, 65 insertions(+), 4 deletions(-) diff --git a/gix-hash/Cargo.toml b/gix-hash/Cargo.toml index 6b970cf4a72..00b46d8eae1 100644 --- a/gix-hash/Cargo.toml +++ b/gix-hash/Cargo.toml @@ -19,6 +19,8 @@ test = false default = ["sha1"] ## Support for SHA-1 digests. sha1 = [] +## Support for SHA256 digests. +sha256 = [] ## Data structures implement `serde::Serialize` and `serde::Deserialize`. serde = ["dep:serde", "faster-hex/serde"] diff --git a/gix-hash/src/hasher.rs b/gix-hash/src/hasher.rs index a214c0ca7f3..4af6112a4ab 100644 --- a/gix-hash/src/hasher.rs +++ b/gix-hash/src/hasher.rs @@ -67,6 +67,7 @@ pub(super) mod _impl { pub fn hasher(kind: crate::Kind) -> Hasher { match kind { crate::Kind::Sha1 => Hasher::default(), + #[cfg(feature = "sha256")] crate::Kind::Sha256 => Hasher::default(), } } diff --git a/gix-hash/src/io.rs b/gix-hash/src/io.rs index f2c91890265..7e681d3dbfe 100644 --- a/gix-hash/src/io.rs +++ b/gix-hash/src/io.rs @@ -118,6 +118,7 @@ pub(super) mod _impl { inner, hash: crate::hasher(object_hash), }, + #[cfg(feature = "sha256")] crate::Kind::Sha256 => Write { inner, hash: crate::hasher(object_hash), diff --git a/gix-hash/src/kind.rs b/gix-hash/src/kind.rs index 8a619521ad7..a7809cd3e32 100644 --- a/gix-hash/src/kind.rs +++ b/gix-hash/src/kind.rs @@ -8,6 +8,8 @@ impl TryFrom for Kind { fn try_from(value: u8) -> Result { Ok(match value { 1 => Kind::Sha1, + #[cfg(feature = "sha256")] + 2 => Kind::Sha256, unknown => return Err(unknown), }) } @@ -19,6 +21,8 @@ impl FromStr for Kind { fn from_str(s: &str) -> Result { Ok(match s { "sha1" | "SHA1" => Kind::Sha1, + #[cfg(feature = "sha256")] + "sha256" | "SHA256" => Kind::Sha256, other => return Err(other.into()), }) } @@ -28,6 +32,7 @@ impl std::fmt::Display for Kind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Kind::Sha1 => f.write_str("SHA1"), + #[cfg(feature = "sha256")] Kind::Sha256 => f.write_str("SHA256"), } } @@ -37,13 +42,27 @@ impl Kind { /// Returns the shortest hash we support. #[inline] pub const fn shortest() -> Self { - Self::Sha1 + #[cfg(all(not(feature = "sha1"), feature = "sha256"))] + { + Self::Sha256 + } + #[cfg(feature = "sha1")] + { + Self::Sha1 + } } /// Returns the longest hash we support. #[inline] pub const fn longest() -> Self { - Self::Sha1 + #[cfg(feature = "sha256")] + { + Self::Sha256 + } + #[cfg(all(not(feature = "sha256"), feature = "sha1"))] + { + Self::Sha1 + } } /// Returns a buffer suitable to hold the longest possible hash in hex. @@ -63,6 +82,7 @@ impl Kind { pub const fn len_in_hex(&self) -> usize { match self { Kind::Sha1 => 40, + #[cfg(feature = "sha256")] Kind::Sha256 => 64, } } @@ -72,6 +92,7 @@ impl Kind { pub const fn len_in_bytes(&self) -> usize { match self { Kind::Sha1 => 20, + #[cfg(feature = "sha256")] Kind::Sha256 => 32, } } @@ -82,6 +103,8 @@ impl Kind { pub const fn from_hex_len(hex_len: usize) -> Option { Some(match hex_len { 0..=40 => Kind::Sha1, + #[cfg(feature = "sha256")] + 0..=64 => Kind::Sha256, _ => return None, }) } @@ -98,6 +121,8 @@ impl Kind { pub(crate) fn from_len_in_bytes(bytes: usize) -> Self { match bytes { 20 => Kind::Sha1, + #[cfg(feature = "sha256")] + 32 => Kind::Sha256, _ => panic!("BUG: must be called only with valid hash lengths produced by len_in_bytes()"), } } @@ -107,6 +132,7 @@ impl Kind { pub fn null_ref(&self) -> &'static oid { match self { Kind::Sha1 => oid::null_sha1(), + #[cfg(feature = "sha256")] Kind::Sha256 => oid::null_sha256(), } } @@ -116,6 +142,7 @@ impl Kind { pub const fn null(&self) -> ObjectId { match self { Kind::Sha1 => ObjectId::null_sha1(), + #[cfg(feature = "sha256")] Kind::Sha256 => ObjectId::null_sha256(), } } diff --git a/gix-hash/src/lib.rs b/gix-hash/src/lib.rs index c2514f6af3d..3b31ff1d1b4 100644 --- a/gix-hash/src/lib.rs +++ b/gix-hash/src/lib.rs @@ -50,6 +50,7 @@ pub struct Prefix { const SIZE_OF_SHA1_DIGEST: usize = 20; /// The size of a SHA256 hash digest in bytes. +#[cfg(feature = "sha256")] const SIZE_OF_SHA256_DIGEST: usize = 32; /// Denotes the kind of function to produce a [`ObjectId`]. @@ -61,6 +62,7 @@ pub enum Kind { #[default] Sha1 = 1, /// The Sha256 hash with 256 bits. + #[cfg(feature = "sha256")] Sha256 = 2, } diff --git a/gix-hash/src/object_id.rs b/gix-hash/src/object_id.rs index 9d4c1b91775..bbee2e82abe 100644 --- a/gix-hash/src/object_id.rs +++ b/gix-hash/src/object_id.rs @@ -4,7 +4,10 @@ use std::{ ops::Deref, }; -use crate::{borrowed::oid, Kind, SIZE_OF_SHA1_DIGEST, SIZE_OF_SHA256_DIGEST}; +use crate::{borrowed::oid, Kind, SIZE_OF_SHA1_DIGEST}; + +#[cfg(feature = "sha256")] +use crate::SIZE_OF_SHA256_DIGEST; /// An owned hash identifying objects, most commonly `Sha1` #[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Copy)] @@ -14,6 +17,7 @@ pub enum ObjectId { /// A SHA 1 hash digest Sha1([u8; SIZE_OF_SHA1_DIGEST]), /// A SHA 256 hash digest + #[cfg(feature = "sha256")] Sha256([u8; SIZE_OF_SHA256_DIGEST]), } @@ -65,6 +69,8 @@ pub mod decode { buf }) }), + #[cfg(feature = "sha256")] + len if len == 2 * SIZE_OF_SHA256_DIGEST => todo!(), len => Err(Error::InvalidHexEncodingLength(len)), } } @@ -86,6 +92,7 @@ impl ObjectId { pub fn kind(&self) -> Kind { match self { ObjectId::Sha1(_) => Kind::Sha1, + #[cfg(feature = "sha256")] ObjectId::Sha256(_) => Kind::Sha256, } } @@ -94,6 +101,7 @@ impl ObjectId { pub fn as_slice(&self) -> &[u8] { match self { Self::Sha1(b) => b.as_ref(), + #[cfg(feature = "sha256")] Self::Sha256(b) => b.as_ref(), } } @@ -102,6 +110,7 @@ impl ObjectId { pub fn as_mut_slice(&mut self) -> &mut [u8] { match self { Self::Sha1(b) => b.as_mut(), + #[cfg(feature = "sha256")] Self::Sha256(b) => b.as_mut(), } } @@ -113,6 +122,7 @@ impl ObjectId { Kind::Sha1 => { ObjectId::Sha1(*b"\xe6\x9d\xe2\x9b\xb2\xd1\xd6\x43\x4b\x8b\x29\xae\x77\x5a\xd8\xc2\xe4\x8c\x53\x91") } + #[cfg(feature = "sha256")] Kind::Sha256 => { ObjectId::Sha256(*b"\x47\x3a\x0f\x4c\x3b\xe8\xa9\x36\x81\xa2\x67\xe3\xb1\xe9\xa7\xdc\xda\x11\x85\x43\x6f\xe1\x41\xf7\x74\x91\x20\xa3\x03\x72\x18\x13") } @@ -126,6 +136,7 @@ impl ObjectId { Kind::Sha1 => { ObjectId::Sha1(*b"\x4b\x82\x5d\xc6\x42\xcb\x6e\xb9\xa0\x60\xe5\x4b\xf8\xd6\x92\x88\xfb\xee\x49\x04") } + #[cfg(feature = "sha256")] Kind::Sha256 => { ObjectId::Sha256(*b"\x6e\xf1\x9b\x41\x22\x5c\x53\x69\xf1\xc1\x04\xd4\x5d\x8d\x85\xef\xa9\xb0\x57\xb5\x3b\x14\xb4\xb9\xb9\x39\xdd\x74\xde\xcc\x53\x21") }, @@ -138,6 +149,7 @@ impl ObjectId { pub const fn null(kind: Kind) -> ObjectId { match kind { Kind::Sha1 => Self::null_sha1(), + #[cfg(feature = "sha256")] Kind::Sha256 => Self::null_sha256(), } } @@ -148,6 +160,7 @@ impl ObjectId { pub fn is_null(&self) -> bool { match self { ObjectId::Sha1(digest) => &digest[..] == oid::null_sha1().as_bytes(), + #[cfg(feature = "sha256")] ObjectId::Sha256(digest) => &digest[..] == oid::null_sha256().as_bytes(), } } @@ -200,6 +213,7 @@ impl ObjectId { /// /// Panics if the slice doesn't have a length of 32. #[inline] + #[cfg(feature = "sha256")] pub(crate) fn from_32_bytes(b: &[u8]) -> ObjectId { let mut id = [0; SIZE_OF_SHA256_DIGEST]; id.copy_from_slice(b); @@ -214,6 +228,7 @@ impl ObjectId { /// Returns a Digest representing a Sha256 whose memory is zeroed. #[inline] + #[cfg(feature = "sha256")] pub(crate) const fn null_sha256() -> ObjectId { ObjectId::Sha256([0u8; SIZE_OF_SHA256_DIGEST]) } @@ -223,6 +238,7 @@ impl std::fmt::Debug for ObjectId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ObjectId::Sha1(_hash) => f.write_str("Sha1(")?, + #[cfg(feature = "sha256")] ObjectId::Sha256(_) => f.write_str("Sha256(")?, } for b in self.as_bytes() { @@ -242,6 +258,7 @@ impl From<&oid> for ObjectId { fn from(v: &oid) -> Self { match v.kind() { Kind::Sha1 => ObjectId::from_20_bytes(v.as_bytes()), + #[cfg(feature = "sha256")] Kind::Sha256 => ObjectId::from_32_bytes(v.as_bytes()), } } diff --git a/gix-hash/src/oid.rs b/gix-hash/src/oid.rs index 89484f791c7..22461f8aea4 100644 --- a/gix-hash/src/oid.rs +++ b/gix-hash/src/oid.rs @@ -1,6 +1,9 @@ use std::hash; -use crate::{Kind, ObjectId, SIZE_OF_SHA1_DIGEST, SIZE_OF_SHA256_DIGEST}; +use crate::{Kind, ObjectId, SIZE_OF_SHA1_DIGEST}; + +#[cfg(feature = "sha256")] +use crate::SIZE_OF_SHA256_DIGEST; /// A borrowed reference to a hash identifying objects. /// @@ -57,6 +60,7 @@ impl std::fmt::Debug for oid { "{}({})", match self.kind() { Kind::Sha1 => "Sha1", + #[cfg(feature = "sha256")] Kind::Sha256 => "Sha256", }, self.to_hex(), @@ -147,6 +151,7 @@ impl oid { pub fn is_null(&self) -> bool { match self.kind() { Kind::Sha1 => &self.bytes == oid::null_sha1().as_bytes(), + #[cfg(feature = "sha256")] Kind::Sha256 => &self.bytes == oid::null_sha256().as_bytes(), } } @@ -156,6 +161,7 @@ impl oid { pub fn is_empty_blob(&self) -> bool { match self.kind() { Kind::Sha1 => &self.bytes == oid::empty_blob_sha1().as_bytes(), + #[cfg(feature = "sha256")] Kind::Sha256 => &self.bytes == oid::empty_blob_sha256().as_bytes(), } } @@ -165,6 +171,7 @@ impl oid { pub fn is_empty_tree(&self) -> bool { match self.kind() { Kind::Sha1 => &self.bytes == oid::empty_tree_sha1().as_bytes(), + #[cfg(feature = "sha256")] Kind::Sha256 => &self.bytes == oid::empty_tree_sha256().as_bytes(), } } @@ -198,6 +205,7 @@ impl oid { /// Returns a Sha256 digest with all bytes being initialized to zero. #[inline] + #[cfg(feature = "sha256")] pub(crate) fn null_sha256() -> &'static Self { oid::from_bytes([0u8; SIZE_OF_SHA256_DIGEST].as_ref()) } @@ -210,6 +218,7 @@ impl oid { /// Returns an oid representing the SHA-256 hash of an empty blob. #[inline] + #[cfg(feature = "sha256")] pub(crate) fn empty_blob_sha256() -> &'static Self { oid::from_bytes(b"\x47\x3a\x0f\x4c\x3b\xe8\xa9\x36\x81\xa2\x67\xe3\xb1\xe9\xa7\xdc\xda\x11\x85\x43\x6f\xe1\x41\xf7\x74\x91\x20\xa3\x03\x72\x18\x13") } @@ -222,6 +231,7 @@ impl oid { /// Returns an oid representing the SHA-256 hash of an empty tree. #[inline] + #[cfg(feature = "sha256")] pub(crate) fn empty_tree_sha256() -> &'static Self { oid::from_bytes( b"\x6e\xf1\x9b\x41\x22\x5c\x53\x69\xf1\xc1\x04\xd4\x5d\x8d\x85\xef\xa9\xb0\x57\xb5\x3b\x14\xb4\xb9\xb9\x39\xdd\x74\xde\xcc\x53\x21", @@ -249,6 +259,7 @@ impl ToOwned for oid { fn to_owned(&self) -> Self::Owned { match self.kind() { Kind::Sha1 => ObjectId::Sha1(self.bytes.try_into().expect("no bug in hash detection")), + #[cfg(feature = "sha256")] Kind::Sha256 => ObjectId::Sha256(self.bytes.try_into().expect("no bug in hash detection")), } } From 3eed1e6d5758d492c91e80a626236559eb5b1a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Fri, 12 Dec 2025 08:17:56 +0100 Subject: [PATCH 3/7] Extract empty blobs and trees --- gix-hash/src/lib.rs | 10 ++++++++++ gix-hash/src/object_id.rs | 24 ++++++++---------------- gix-hash/src/oid.rs | 14 ++++++-------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/gix-hash/src/lib.rs b/gix-hash/src/lib.rs index 3b31ff1d1b4..1f8633bf9db 100644 --- a/gix-hash/src/lib.rs +++ b/gix-hash/src/lib.rs @@ -53,6 +53,16 @@ const SIZE_OF_SHA1_DIGEST: usize = 20; #[cfg(feature = "sha256")] const SIZE_OF_SHA256_DIGEST: usize = 32; +const EMPTY_BLOB_SHA1: &[u8; SIZE_OF_SHA1_DIGEST] = + b"\xe6\x9d\xe2\x9b\xb2\xd1\xd6\x43\x4b\x8b\x29\xae\x77\x5a\xd8\xc2\xe4\x8c\x53\x91"; +const EMPTY_TREE_SHA1: &[u8; SIZE_OF_SHA1_DIGEST] = + b"\x4b\x82\x5d\xc6\x42\xcb\x6e\xb9\xa0\x60\xe5\x4b\xf8\xd6\x92\x88\xfb\xee\x49\x04"; + +#[cfg(feature = "sha256")] +const EMPTY_BLOB_SHA256: &[u8; SIZE_OF_SHA256_DIGEST] = b"\x47\x3a\x0f\x4c\x3b\xe8\xa9\x36\x81\xa2\x67\xe3\xb1\xe9\xa7\xdc\xda\x11\x85\x43\x6f\xe1\x41\xf7\x74\x91\x20\xa3\x03\x72\x18\x13"; +#[cfg(feature = "sha256")] +const EMPTY_TREE_SHA256: &[u8; SIZE_OF_SHA256_DIGEST] = b"\x6e\xf1\x9b\x41\x22\x5c\x53\x69\xf1\xc1\x04\xd4\x5d\x8d\x85\xef\xa9\xb0\x57\xb5\x3b\x14\xb4\xb9\xb9\x39\xdd\x74\xde\xcc\x53\x21"; + /// Denotes the kind of function to produce a [`ObjectId`]. #[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/gix-hash/src/object_id.rs b/gix-hash/src/object_id.rs index bbee2e82abe..667e2aa2e61 100644 --- a/gix-hash/src/object_id.rs +++ b/gix-hash/src/object_id.rs @@ -4,10 +4,10 @@ use std::{ ops::Deref, }; -use crate::{borrowed::oid, Kind, SIZE_OF_SHA1_DIGEST}; +use crate::{borrowed::oid, Kind, EMPTY_BLOB_SHA1, EMPTY_TREE_SHA1, SIZE_OF_SHA1_DIGEST}; #[cfg(feature = "sha256")] -use crate::SIZE_OF_SHA256_DIGEST; +use crate::{EMPTY_BLOB_SHA256, EMPTY_TREE_SHA256, SIZE_OF_SHA256_DIGEST}; /// An owned hash identifying objects, most commonly `Sha1` #[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Copy)] @@ -119,13 +119,9 @@ impl ObjectId { #[inline] pub const fn empty_blob(hash: Kind) -> ObjectId { match hash { - Kind::Sha1 => { - ObjectId::Sha1(*b"\xe6\x9d\xe2\x9b\xb2\xd1\xd6\x43\x4b\x8b\x29\xae\x77\x5a\xd8\xc2\xe4\x8c\x53\x91") - } - #[cfg(feature = "sha256")] - Kind::Sha256 => { - ObjectId::Sha256(*b"\x47\x3a\x0f\x4c\x3b\xe8\xa9\x36\x81\xa2\x67\xe3\xb1\xe9\xa7\xdc\xda\x11\x85\x43\x6f\xe1\x41\xf7\x74\x91\x20\xa3\x03\x72\x18\x13") - } + Kind::Sha1 => ObjectId::Sha1(*EMPTY_BLOB_SHA1), + #[cfg(feature = "sha256")] + Kind::Sha256 => ObjectId::Sha256(*EMPTY_BLOB_SHA256), } } @@ -133,13 +129,9 @@ impl ObjectId { #[inline] pub const fn empty_tree(hash: Kind) -> ObjectId { match hash { - Kind::Sha1 => { - ObjectId::Sha1(*b"\x4b\x82\x5d\xc6\x42\xcb\x6e\xb9\xa0\x60\xe5\x4b\xf8\xd6\x92\x88\xfb\xee\x49\x04") - } - #[cfg(feature = "sha256")] - Kind::Sha256 => { - ObjectId::Sha256(*b"\x6e\xf1\x9b\x41\x22\x5c\x53\x69\xf1\xc1\x04\xd4\x5d\x8d\x85\xef\xa9\xb0\x57\xb5\x3b\x14\xb4\xb9\xb9\x39\xdd\x74\xde\xcc\x53\x21") - }, + Kind::Sha1 => ObjectId::Sha1(*EMPTY_TREE_SHA1), + #[cfg(feature = "sha256")] + Kind::Sha256 => ObjectId::Sha256(*EMPTY_TREE_SHA256), } } diff --git a/gix-hash/src/oid.rs b/gix-hash/src/oid.rs index 22461f8aea4..35ea9aebe68 100644 --- a/gix-hash/src/oid.rs +++ b/gix-hash/src/oid.rs @@ -1,9 +1,9 @@ use std::hash; -use crate::{Kind, ObjectId, SIZE_OF_SHA1_DIGEST}; +use crate::{Kind, ObjectId, EMPTY_BLOB_SHA1, EMPTY_TREE_SHA1, SIZE_OF_SHA1_DIGEST}; #[cfg(feature = "sha256")] -use crate::SIZE_OF_SHA256_DIGEST; +use crate::{EMPTY_BLOB_SHA256, EMPTY_TREE_SHA256, SIZE_OF_SHA256_DIGEST}; /// A borrowed reference to a hash identifying objects. /// @@ -213,29 +213,27 @@ impl oid { /// Returns an oid representing the SHA-1 hash of an empty blob. #[inline] pub(crate) fn empty_blob_sha1() -> &'static Self { - oid::from_bytes(b"\xe6\x9d\xe2\x9b\xb2\xd1\xd6\x43\x4b\x8b\x29\xae\x77\x5a\xd8\xc2\xe4\x8c\x53\x91") + oid::from_bytes(EMPTY_BLOB_SHA1) } /// Returns an oid representing the SHA-256 hash of an empty blob. #[inline] #[cfg(feature = "sha256")] pub(crate) fn empty_blob_sha256() -> &'static Self { - oid::from_bytes(b"\x47\x3a\x0f\x4c\x3b\xe8\xa9\x36\x81\xa2\x67\xe3\xb1\xe9\xa7\xdc\xda\x11\x85\x43\x6f\xe1\x41\xf7\x74\x91\x20\xa3\x03\x72\x18\x13") + oid::from_bytes(EMPTY_BLOB_SHA256) } /// Returns an oid representing the SHA-1 hash of an empty tree. #[inline] pub(crate) fn empty_tree_sha1() -> &'static Self { - oid::from_bytes(b"\x4b\x82\x5d\xc6\x42\xcb\x6e\xb9\xa0\x60\xe5\x4b\xf8\xd6\x92\x88\xfb\xee\x49\x04") + oid::from_bytes(EMPTY_TREE_SHA1) } /// Returns an oid representing the SHA-256 hash of an empty tree. #[inline] #[cfg(feature = "sha256")] pub(crate) fn empty_tree_sha256() -> &'static Self { - oid::from_bytes( - b"\x6e\xf1\x9b\x41\x22\x5c\x53\x69\xf1\xc1\x04\xd4\x5d\x8d\x85\xef\xa9\xb0\x57\xb5\x3b\x14\xb4\xb9\xb9\x39\xdd\x74\xde\xcc\x53\x21", - ) + oid::from_bytes(EMPTY_TREE_SHA256) } } From ed3ac8101444f6c654ea4efd766b10c06ffde49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Fri, 12 Dec 2025 08:30:08 +0100 Subject: [PATCH 4/7] Replace hardcoded sizes by constants --- gix-hash/src/kind.rs | 21 ++++++++++++--------- gix-hash/src/lib.rs | 5 +++++ gix-hash/src/object_id.rs | 17 ++++++++++------- gix-hash/src/oid.rs | 9 ++++++++- 4 files changed, 35 insertions(+), 17 deletions(-) diff --git a/gix-hash/src/kind.rs b/gix-hash/src/kind.rs index a7809cd3e32..362b6cba71c 100644 --- a/gix-hash/src/kind.rs +++ b/gix-hash/src/kind.rs @@ -1,6 +1,9 @@ use std::str::FromStr; -use crate::{oid, Kind, ObjectId}; +use crate::{oid, Kind, ObjectId, SIZE_OF_SHA1_DIGEST, SIZE_OF_SHA1_HEX_DIGEST}; + +#[cfg(feature = "sha256")] +use crate::{SIZE_OF_SHA256_DIGEST, SIZE_OF_SHA256_HEX_DIGEST}; impl TryFrom for Kind { type Error = u8; @@ -81,9 +84,9 @@ impl Kind { #[inline] pub const fn len_in_hex(&self) -> usize { match self { - Kind::Sha1 => 40, + Kind::Sha1 => SIZE_OF_SHA1_HEX_DIGEST, #[cfg(feature = "sha256")] - Kind::Sha256 => 64, + Kind::Sha256 => SIZE_OF_SHA256_HEX_DIGEST, } } @@ -91,9 +94,9 @@ impl Kind { #[inline] pub const fn len_in_bytes(&self) -> usize { match self { - Kind::Sha1 => 20, + Kind::Sha1 => SIZE_OF_SHA1_DIGEST, #[cfg(feature = "sha256")] - Kind::Sha256 => 32, + Kind::Sha256 => SIZE_OF_SHA256_DIGEST, } } @@ -102,9 +105,9 @@ impl Kind { #[inline] pub const fn from_hex_len(hex_len: usize) -> Option { Some(match hex_len { - 0..=40 => Kind::Sha1, + 0..=SIZE_OF_SHA1_HEX_DIGEST => Kind::Sha1, #[cfg(feature = "sha256")] - 0..=64 => Kind::Sha256, + 0..=SIZE_OF_SHA256_HEX_DIGEST => Kind::Sha256, _ => return None, }) } @@ -120,9 +123,9 @@ impl Kind { #[inline] pub(crate) fn from_len_in_bytes(bytes: usize) -> Self { match bytes { - 20 => Kind::Sha1, + SIZE_OF_SHA1_DIGEST => Kind::Sha1, #[cfg(feature = "sha256")] - 32 => Kind::Sha256, + SIZE_OF_SHA256_DIGEST => Kind::Sha256, _ => panic!("BUG: must be called only with valid hash lengths produced by len_in_bytes()"), } } diff --git a/gix-hash/src/lib.rs b/gix-hash/src/lib.rs index 1f8633bf9db..8e6f014c486 100644 --- a/gix-hash/src/lib.rs +++ b/gix-hash/src/lib.rs @@ -48,10 +48,15 @@ pub struct Prefix { /// The size of a SHA1 hash digest in bytes. const SIZE_OF_SHA1_DIGEST: usize = 20; +/// The size of a SHA1 hash digest in hex. +const SIZE_OF_SHA1_HEX_DIGEST: usize = 2 * SIZE_OF_SHA1_DIGEST; /// The size of a SHA256 hash digest in bytes. #[cfg(feature = "sha256")] const SIZE_OF_SHA256_DIGEST: usize = 32; +/// The size of a SHA256 hash digest in hex. +#[cfg(feature = "sha256")] +const SIZE_OF_SHA256_HEX_DIGEST: usize = 2 * SIZE_OF_SHA256_DIGEST; const EMPTY_BLOB_SHA1: &[u8; SIZE_OF_SHA1_DIGEST] = b"\xe6\x9d\xe2\x9b\xb2\xd1\xd6\x43\x4b\x8b\x29\xae\x77\x5a\xd8\xc2\xe4\x8c\x53\x91"; diff --git a/gix-hash/src/object_id.rs b/gix-hash/src/object_id.rs index 667e2aa2e61..fe427ad5620 100644 --- a/gix-hash/src/object_id.rs +++ b/gix-hash/src/object_id.rs @@ -38,7 +38,10 @@ impl Hash for ObjectId { pub mod decode { use std::str::FromStr; - use crate::object_id::ObjectId; + use crate::{object_id::ObjectId, SIZE_OF_SHA1_DIGEST, SIZE_OF_SHA1_HEX_DIGEST}; + + #[cfg(feature = "sha256")] + use crate::{SIZE_OF_SHA256_DIGEST, SIZE_OF_SHA256_HEX_DIGEST}; /// An error returned by [`ObjectId::from_hex()`][crate::ObjectId::from_hex()] #[derive(Debug, thiserror::Error)] @@ -57,9 +60,9 @@ pub mod decode { /// Such a buffer can be obtained using [`oid::write_hex_to(buffer)`][super::oid::write_hex_to()] pub fn from_hex(buffer: &[u8]) -> Result { match buffer.len() { - 40 => Ok({ + SIZE_OF_SHA1_HEX_DIGEST => Ok({ ObjectId::Sha1({ - let mut buf = [0; 20]; + let mut buf = [0; SIZE_OF_SHA1_DIGEST]; faster_hex::hex_decode(buffer, &mut buf).map_err(|err| match err { faster_hex::Error::InvalidChar | faster_hex::Error::Overflow => Error::Invalid, faster_hex::Error::InvalidLength(_) => { @@ -70,7 +73,7 @@ pub mod decode { }) }), #[cfg(feature = "sha256")] - len if len == 2 * SIZE_OF_SHA256_DIGEST => todo!(), + SIZE_OF_SHA256_HEX_DIGEST => todo!(), len => Err(Error::InvalidHexEncodingLength(len)), } } @@ -177,7 +180,7 @@ impl ObjectId { /// Use `Self::try_from(bytes)` for a fallible version. pub fn from_bytes_or_panic(bytes: &[u8]) -> Self { match bytes.len() { - 20 => Self::Sha1(bytes.try_into().expect("prior length validation")), + SIZE_OF_SHA1_DIGEST => Self::Sha1(bytes.try_into().expect("prior length validation")), other => panic!("BUG: unsupported hash len: {other}"), } } @@ -215,7 +218,7 @@ impl ObjectId { /// Returns a Digest representing a Sha1 whose memory is zeroed. #[inline] pub(crate) const fn null_sha1() -> ObjectId { - ObjectId::Sha1([0u8; 20]) + ObjectId::Sha1([0u8; SIZE_OF_SHA1_DIGEST]) } /// Returns a Digest representing a Sha256 whose memory is zeroed. @@ -241,7 +244,7 @@ impl std::fmt::Debug for ObjectId { } impl From<[u8; SIZE_OF_SHA1_DIGEST]> for ObjectId { - fn from(v: [u8; 20]) -> Self { + fn from(v: [u8; SIZE_OF_SHA1_DIGEST]) -> Self { Self::new_sha1(v) } } diff --git a/gix-hash/src/oid.rs b/gix-hash/src/oid.rs index 35ea9aebe68..a67463ebae6 100644 --- a/gix-hash/src/oid.rs +++ b/gix-hash/src/oid.rs @@ -82,7 +82,14 @@ impl oid { #[inline] pub fn try_from_bytes(digest: &[u8]) -> Result<&Self, Error> { match digest.len() { - 20 => Ok( + SIZE_OF_SHA1_DIGEST => Ok( + #[allow(unsafe_code)] + unsafe { + &*(std::ptr::from_ref::<[u8]>(digest) as *const oid) + }, + ), + #[cfg(feature = "sha256")] + SIZE_OF_SHA256_DIGEST => Ok( #[allow(unsafe_code)] unsafe { &*(std::ptr::from_ref::<[u8]>(digest) as *const oid) From 53d3b4c1e67010583d58daf5b0f82ac6b9b0bbe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Fri, 12 Dec 2025 13:00:17 +0100 Subject: [PATCH 5/7] Implement match arm for SHA256 --- gix-hash/src/object_id.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/gix-hash/src/object_id.rs b/gix-hash/src/object_id.rs index fe427ad5620..e2f8a8d20ac 100644 --- a/gix-hash/src/object_id.rs +++ b/gix-hash/src/object_id.rs @@ -73,7 +73,18 @@ pub mod decode { }) }), #[cfg(feature = "sha256")] - SIZE_OF_SHA256_HEX_DIGEST => todo!(), + SIZE_OF_SHA256_HEX_DIGEST => Ok({ + ObjectId::Sha256({ + let mut buf = [0; SIZE_OF_SHA256_DIGEST]; + faster_hex::hex_decode(buffer, &mut buf).map_err(|err| match err { + faster_hex::Error::InvalidChar | faster_hex::Error::Overflow => Error::Invalid, + faster_hex::Error::InvalidLength(_) => { + unreachable!("BUG: This is already checked") + } + })?; + buf + }) + }), len => Err(Error::InvalidHexEncodingLength(len)), } } From 0c9c0de4df90a676b04829b028bcff251501826d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Fri, 12 Dec 2025 13:13:22 +0100 Subject: [PATCH 6/7] Add a few tests --- gix-hash/tests/hash/hasher.rs | 13 ++++++++++++- gix-hash/tests/hash/oid.rs | 7 +++++++ gix-hash/tests/hash/prefix.rs | 10 ++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/gix-hash/tests/hash/hasher.rs b/gix-hash/tests/hash/hasher.rs index ee73631d69c..196a3a1e407 100644 --- a/gix-hash/tests/hash/hasher.rs +++ b/gix-hash/tests/hash/hasher.rs @@ -14,10 +14,21 @@ fn size_of_hasher() { } #[test] -fn size_of_try_finalize_return_type() { +#[cfg(all(not(feature = "sha256"), feature = "sha1"))] +fn size_of_try_finalize_return_type_sha1_only() { assert_eq!( std::mem::size_of::>(), 21, "The size of the return value is just 1 byte larger than just returning the object hash itself" ); } + +#[test] +#[cfg(all(feature = "sha256", feature = "sha1"))] +fn size_of_try_finalize_return_type_sha1_and_sha256() { + assert_eq!( + std::mem::size_of::>(), + 34, + "The size of the return value is just 1 byte larger than just returning the object hash itself" + ); +} diff --git a/gix-hash/tests/hash/oid.rs b/gix-hash/tests/hash/oid.rs index e1e23e7840a..a0a344d1639 100644 --- a/gix-hash/tests/hash/oid.rs +++ b/gix-hash/tests/hash/oid.rs @@ -20,6 +20,13 @@ fn is_null() { assert!(gix_hash::Kind::Sha1.null().as_ref().is_null()); } +#[test] +#[cfg(feature = "sha256")] +fn is_null_sha256() { + assert!(gix_hash::Kind::Sha256.null().is_null()); + assert!(gix_hash::Kind::Sha256.null().as_ref().is_null()); +} + #[test] fn is_empty_blob() { let empty_blob = gix_hash::ObjectId::empty_blob(gix_hash::Kind::Sha1); diff --git a/gix-hash/tests/hash/prefix.rs b/gix-hash/tests/hash/prefix.rs index 4959fd7149f..b8e06f4d78a 100644 --- a/gix-hash/tests/hash/prefix.rs +++ b/gix-hash/tests/hash/prefix.rs @@ -107,6 +107,7 @@ mod try_from { } #[test] + #[cfg(all(not(feature = "sha256"), feature = "sha1"))] fn id_to_long() { let input = "abcdefabcdefabcdefabcdefabcdefabcdefabcd123123123123123123"; let expected = Error::TooLong { hex_len: 58 }; @@ -114,6 +115,15 @@ mod try_from { assert_eq!(actual, expected); } + #[test] + #[cfg(all(feature = "sha256", feature = "sha1"))] + fn id_to_long() { + let input = "abcdefabcdefabcdefabcdefabcdefabcdefabcd123123123123123123123123123123"; + let expected = Error::TooLong { hex_len: 70 }; + let actual = Prefix::try_from(input).unwrap_err(); + assert_eq!(actual, expected); + } + #[test] fn invalid_chars() { let input = "abcdfOsd"; From 71021ad3d2b14b60cece47304c9b33f55abfcb1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Fri, 12 Dec 2025 14:26:47 +0100 Subject: [PATCH 7/7] Add more tests --- gix-hash/tests/hash/prefix.rs | 63 ++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/gix-hash/tests/hash/prefix.rs b/gix-hash/tests/hash/prefix.rs index b8e06f4d78a..06d2907756e 100644 --- a/gix-hash/tests/hash/prefix.rs +++ b/gix-hash/tests/hash/prefix.rs @@ -4,7 +4,7 @@ mod cmp_oid { use crate::hex_to_id; #[test] - fn it_detects_inequality() { + fn it_detects_inequality_sha1() { let prefix = gix_hash::Prefix::new(&hex_to_id("b920bbb055e1efb9080592a409d3975738b6efb3"), 7).unwrap(); assert_eq!( prefix.cmp_oid(&hex_to_id("a920bbb055e1efb9080592a409d3975738b6efb3")), @@ -18,7 +18,30 @@ mod cmp_oid { } #[test] - fn it_detects_equality() { + #[cfg(feature = "sha256")] + fn it_detects_inequality_sha256() { + let prefix = gix_hash::Prefix::new( + &hex_to_id("b920bbb055e1efb9080592a409d3975738b6efb338b6efb338b6efb338b6efb3"), + 7, + ) + .unwrap(); + assert_eq!( + prefix.cmp_oid(&hex_to_id( + "a920bbb055e1efb9080592a409d3975738b6efb338b6efb338b6efb338b6efb3" + )), + Ordering::Greater + ); + assert_eq!( + prefix.cmp_oid(&hex_to_id( + "b920bbf055e1efb9080592a409d3975738b6efb338b6efb338b6efb338b6efb3" + )), + Ordering::Less + ); + assert_eq!(prefix.to_string(), "b920bbb"); + } + + #[test] + fn it_detects_equality_sha1() { let id = hex_to_id("a920bbb055e1efb9080592a409d3975738b6efb3"); let prefix = gix_hash::Prefix::new(&id, 6).unwrap(); assert_eq!(prefix.cmp_oid(&id), Ordering::Equal); @@ -28,6 +51,19 @@ mod cmp_oid { ); assert_eq!(prefix.to_string(), "a920bb"); } + + #[test] + #[cfg(feature = "sha256")] + fn it_detects_equality_sha256() { + let id = hex_to_id("a920bbb055e1efb9080592a409d3975738b6efb338b6efb338b6efb338b6efb3"); + let prefix = gix_hash::Prefix::new(&id, 6).unwrap(); + assert_eq!(prefix.cmp_oid(&id), Ordering::Equal); + assert_eq!( + prefix.cmp_oid(&hex_to_id("a920bbffffffffffffffffffffffffffffffffff")), + Ordering::Equal + ); + assert_eq!(prefix.to_string(), "a920bb"); + } } mod new { @@ -38,7 +74,7 @@ mod new { use crate::hex_to_id; #[test] - fn various_valid_inputs() { + fn various_valid_inputs_sha1() { let oid_hex = "abcdefabcdefabcdefabcdefabcdefabcdefabcd"; let oid = hex_to_id(oid_hex); @@ -53,6 +89,23 @@ mod new { } } + #[test] + #[cfg(feature = "sha256")] + fn various_valid_inputs_sha256() { + let oid_hex = "abcdefabcdefabcdefabcdefabcdefabcdefabcdedabcdedabcdedabcdedabcd"; + let oid = hex_to_id(oid_hex); + + for hex_len in 4..oid.kind().len_in_hex() { + let mut expected = String::from(&oid_hex[..hex_len]); + let num_of_zeros = oid.kind().len_in_hex() - hex_len; + expected.extend(std::iter::repeat_n('0', num_of_zeros)); + let prefix = gix_hash::Prefix::new(&oid, hex_len).unwrap(); + assert_eq!(prefix.as_oid().to_hex().to_string(), expected, "{hex_len}"); + assert_eq!(prefix.hex_len(), hex_len); + assert_eq!(prefix.cmp_oid(&oid), Ordering::Equal); + } + } + #[test] fn errors_if_hex_len_is_longer_than_oid_len_in_hex() { let kind = Kind::Sha1; @@ -108,7 +161,7 @@ mod try_from { #[test] #[cfg(all(not(feature = "sha256"), feature = "sha1"))] - fn id_to_long() { + fn id_too_long() { let input = "abcdefabcdefabcdefabcdefabcdefabcdefabcd123123123123123123"; let expected = Error::TooLong { hex_len: 58 }; let actual = Prefix::try_from(input).unwrap_err(); @@ -117,7 +170,7 @@ mod try_from { #[test] #[cfg(all(feature = "sha256", feature = "sha1"))] - fn id_to_long() { + fn id_too_long() { let input = "abcdefabcdefabcdefabcdefabcdefabcdefabcd123123123123123123123123123123"; let expected = Error::TooLong { hex_len: 70 }; let actual = Prefix::try_from(input).unwrap_err();