#![warn(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::indexing_slicing,
reason = "parses untrusted repository or network input (#103)"
)]
use std::fmt;
use std::hash::Hash;
use std::str::FromStr;
mod id_hash;
mod prefix;
pub(crate) use id_hash::{IdMap, IdSet, PeerIdMap, PeerIdSet};
pub use prefix::{ObjectIdPrefix, ParseObjectIdPrefixError};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ObjectKind {
Blob,
Tree,
Commit,
Tag,
}
impl ObjectKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Blob => "blob",
Self::Tree => "tree",
Self::Commit => "commit",
Self::Tag => "tag",
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum ObjectFormat {
Sha1,
Sha256,
}
impl fmt::Display for ObjectFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Sha1 => "sha1",
Self::Sha256 => "sha256",
})
}
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
#[non_exhaustive]
pub enum ObjectId {
Sha1([u8; 20]),
Sha256([u8; 32]),
}
impl Hash for ObjectId {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
state.write(self.as_bytes());
}
}
impl ObjectId {
pub fn for_blob(format: ObjectFormat, bytes: &[u8]) -> Self {
format.hash_object(crate::ObjectKind::Blob, bytes)
}
#[cfg(test)]
pub(crate) fn for_object(kind: &str, bytes: &[u8]) -> Self {
hash_sha1(kind, bytes)
}
pub fn from_bytes(format: ObjectFormat, bytes: &[u8]) -> Result<Self, ParseObjectIdError> {
match format {
ObjectFormat::Sha1 => bytes.try_into().map(Self::Sha1),
ObjectFormat::Sha256 => bytes.try_into().map(Self::Sha256),
}
.map_err(|_| ParseObjectIdError)
}
pub fn from_hex(format: ObjectFormat, value: &str) -> Result<Self, ParseObjectIdError> {
let id: Self = value.parse()?;
if id.format() != format {
return Err(ParseObjectIdError);
}
Ok(id)
}
pub const fn format(self) -> ObjectFormat {
match self {
Self::Sha1(_) => ObjectFormat::Sha1,
Self::Sha256(_) => ObjectFormat::Sha256,
}
}
pub const fn as_bytes(&self) -> &[u8] {
match self {
Self::Sha1(bytes) => bytes,
Self::Sha256(bytes) => bytes,
}
}
pub const fn null(format: ObjectFormat) -> Self {
match format {
ObjectFormat::Sha1 => Self::Sha1([0; 20]),
ObjectFormat::Sha256 => Self::Sha256([0; 32]),
}
}
pub fn is_null(self) -> bool {
self.as_bytes().iter().all(|&b| b == 0)
}
pub(crate) fn require_format(self, expected: ObjectFormat) -> Result<(), ObjectFormatError> {
if self.format() == expected {
Ok(())
} else {
Err(ObjectFormatError {
expected,
actual: self.format(),
})
}
}
}
impl ObjectFormat {
pub const fn digest_len(self) -> usize {
match self {
Self::Sha1 => 20,
Self::Sha256 => 32,
}
}
pub fn hash_object(self, kind: crate::ObjectKind, bytes: &[u8]) -> ObjectId {
#[cfg(test)]
HASHES.with(|hashes| hashes.set(hashes.get() + 1));
match self {
Self::Sha1 => hash_sha1(kind.as_str(), bytes),
Self::Sha256 => {
let mut hash = aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA256);
hash.update(object_header(kind.as_str(), bytes.len()).as_bytes());
hash.update(bytes);
ObjectId::Sha256(digest(hash))
}
}
}
}
#[derive(Clone)]
pub(crate) enum Hasher {
Sha1(aws_lc_rs::digest::Context),
Sha256(aws_lc_rs::digest::Context),
}
impl Hasher {
pub(crate) fn new(format: ObjectFormat) -> Self {
match format {
ObjectFormat::Sha1 => Self::Sha1(aws_lc_rs::digest::Context::new(
&aws_lc_rs::digest::SHA1_FOR_LEGACY_USE_ONLY,
)),
ObjectFormat::Sha256 => {
Self::Sha256(aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA256))
}
}
}
pub(crate) fn update(&mut self, bytes: &[u8]) {
match self {
Self::Sha1(hash) => hash.update(bytes),
Self::Sha256(hash) => hash.update(bytes),
}
}
pub(crate) fn finalize(self) -> ObjectId {
match self {
Self::Sha1(hash) => ObjectId::Sha1(digest(hash)),
Self::Sha256(hash) => ObjectId::Sha256(digest(hash)),
}
}
}
impl ObjectFormat {
pub(crate) fn checksum(self, bytes: &[u8]) -> ObjectId {
let mut hash = Hasher::new(self);
hash.update(bytes);
hash.finalize()
}
}
fn hash_sha1(kind: &str, bytes: &[u8]) -> ObjectId {
let mut hash = aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA1_FOR_LEGACY_USE_ONLY);
hash.update(object_header(kind, bytes.len()).as_bytes());
hash.update(bytes);
ObjectId::Sha1(digest(hash))
}
#[expect(
clippy::expect_used,
reason = "SHA-1 and SHA-256 contexts always produce 20 and 32 bytes, matching each caller's N"
)]
fn digest<const N: usize>(hash: aws_lc_rs::digest::Context) -> [u8; N] {
hash.finish()
.as_ref()
.try_into()
.expect("digest length matches the selected algorithm")
}
impl fmt::Display for ObjectId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for byte in self.as_bytes() {
write!(f, "{byte:02x}")?;
}
Ok(())
}
}
impl FromStr for ObjectId {
type Err = ParseObjectIdError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let format = match value.len() {
40 => ObjectFormat::Sha1,
64 => ObjectFormat::Sha256,
_ => return Err(ParseObjectIdError),
};
let mut id = Self::null(format);
let bytes: &mut [u8] = match &mut id {
Self::Sha1(bytes) => bytes,
Self::Sha256(bytes) => bytes,
};
let (pairs, _) = value.as_bytes().as_chunks::<2>();
for (byte, [high, low]) in bytes.iter_mut().zip(pairs) {
*byte = hex_digit(*high)? << 4 | hex_digit(*low)?;
}
Ok(id)
}
}
fn hex_digit(digit: u8) -> Result<u8, ParseObjectIdError> {
match digit {
b'0'..=b'9' => Ok(digit - b'0'),
b'a'..=b'f' => Ok(digit - b'a' + 10),
b'A'..=b'F' => Ok(digit - b'A' + 10),
_ => Err(ParseObjectIdError),
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
#[error("expected a format-sized digest or 40/64 hexadecimal digits")]
pub struct ParseObjectIdError;
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
#[error("object format mismatch: expected {expected}, got {actual}")]
pub struct ObjectFormatError {
pub expected: ObjectFormat,
pub actual: ObjectFormat,
}
pub fn encode_blob(bytes: &[u8]) -> Vec<u8> {
let mut encoded = object_header("blob", bytes.len()).into_bytes();
encoded.extend_from_slice(bytes);
encoded
}
pub(crate) fn object_header(kind: &str, length: usize) -> String {
format!("{kind} {length}\0")
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case::nine_bytes(vec![b'x'; 9], b"blob 9\0", "aab1169962b9fd16c9465117c12cad0ffd8bffac")]
#[case::ten_bytes(vec![b'x'; 10], b"blob 10\0", "72035e10b5524757f990eb198acfce358b268c12")]
#[case::ninety_nine_bytes(vec![b'x'; 99], b"blob 99\0", "9e84ea89f5f5200b6ef0350a87885d5d3f39e453")]
#[case::one_hundred_bytes(vec![b'x'; 100], b"blob 100\0", "f6be7cae2045aac11912ea642bf7f9d5d261f63b")]
#[case::all_byte_values((0..=255).collect(), b"blob 256\0", "c86626638e0bc8cf47ca49bb1525b40e9737ee64")]
fn encodes_and_identifies_blob_bytes(
#[case] bytes: Vec<u8>,
#[case] header: &[u8],
#[case] expected_id: &str,
) {
let expected_encoding = [header, bytes.as_slice()].concat();
assert_eq!(encode_blob(&bytes), expected_encoding);
assert_eq!(
ObjectId::for_blob(crate::ObjectFormat::Sha1, &bytes).to_string(),
expected_id
);
}
#[rstest]
#[case::sha1(ObjectFormat::Sha1, "sha1")]
#[case::sha256(ObjectFormat::Sha256, "sha256")]
fn displays_git_object_format_names(#[case] format: ObjectFormat, #[case] expected: &str) {
assert_eq!(format.to_string(), expected);
}
#[rstest]
#[case::lowercase("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391")]
#[case::uppercase("E69DE29BB2D1D6434B8B29AE775AD8C2E48C5391")]
fn parses_full_sha1_identifiers(#[case] value: &str) {
let id = ObjectId::for_blob(crate::ObjectFormat::Sha1, b"");
assert_eq!(value.parse(), Ok(id));
}
#[test]
fn preserves_raw_identity_bytes() {
let id = ObjectId::for_blob(crate::ObjectFormat::Sha1, b"");
assert_eq!(
ObjectId::from_bytes(id.format(), id.as_bytes()).unwrap(),
id
);
}
#[rstest]
#[case::abbreviated("abc".to_owned())]
#[case::non_hexadecimal("g".repeat(40))]
#[case::non_ascii("é".repeat(20))]
fn rejects_invalid_sha1_identifiers(#[case] value: String) {
assert_eq!(value.parse::<ObjectId>(), Err(ParseObjectIdError));
}
}
#[cfg(test)]
mod format_tests {
use std::collections::{BTreeMap, HashMap};
use rstest::rstest;
use super::*;
#[rstest]
#[case::sha1(ObjectFormat::Sha1, 20)]
#[case::sha256(ObjectFormat::Sha256, 32)]
fn preserves_full_digest_and_null(#[case] format: ObjectFormat, #[case] length: usize) {
let mut bytes = vec![0; length];
bytes[length - 1] = 0xff;
let id = ObjectId::from_bytes(format, &bytes).unwrap();
assert_eq!(id.as_bytes(), bytes);
assert_eq!(format.digest_len(), length);
assert!(!id.is_null());
assert!(ObjectId::null(format).is_null());
assert_eq!(ObjectId::null(format).to_string(), "0".repeat(length * 2));
assert_eq!(
ObjectId::from_hex(format, &id.to_string().to_uppercase()),
Ok(id)
);
}
#[rstest]
#[case::sha1_short(ObjectFormat::Sha1, 19)]
#[case::sha1_long(ObjectFormat::Sha1, 21)]
#[case::sha1_other(ObjectFormat::Sha1, 32)]
#[case::sha256_short(ObjectFormat::Sha256, 31)]
#[case::sha256_long(ObjectFormat::Sha256, 33)]
#[case::sha256_other(ObjectFormat::Sha256, 20)]
fn rejects_wrong_raw_width(#[case] format: ObjectFormat, #[case] length: usize) {
assert_eq!(
ObjectId::from_bytes(format, &vec![0; length]),
Err(ParseObjectIdError)
);
}
#[rstest]
#[case::short_sha1("0".repeat(39))]
#[case::long_sha1("0".repeat(41))]
#[case::short_sha256("0".repeat(63))]
#[case::long_sha256("0".repeat(65))]
#[case::invalid_sha256("g".repeat(64))]
#[case::unicode_sha256("é".repeat(32))]
#[case::last_digit(format!("{}g", "0".repeat(39)))]
#[case::sign(format!("+{}", "0".repeat(39)))]
#[case::between_ranges(":@`G".repeat(10))]
fn rejects_invalid_hex(#[case] value: String) {
assert_eq!(value.parse::<ObjectId>(), Err(ParseObjectIdError));
}
#[rstest]
#[case::sha1(ObjectFormat::Sha1, ObjectFormat::Sha256)]
#[case::sha256(ObjectFormat::Sha256, ObjectFormat::Sha1)]
fn rejects_other_formats_hex(#[case] expected: ObjectFormat, #[case] actual: ObjectFormat) {
assert_eq!(
ObjectId::from_hex(expected, &ObjectId::null(actual).to_string()),
Err(ParseObjectIdError)
);
}
#[test]
fn mixed_format_keys_preserve_all_bytes() {
let sha1 = ObjectId::null(ObjectFormat::Sha1);
let sha256 = ObjectId::null(ObjectFormat::Sha256);
let mut bytes = [0; 32];
bytes[31] = 1;
let tail = ObjectId::Sha256(bytes);
let ordered = BTreeMap::from([(tail, "tail"), (sha256, "sha256"), (sha1, "sha1")]);
let hashed = HashMap::from([(sha1, 1), (sha256, 2), (tail, 3)]);
assert_eq!(
ordered.into_keys().collect::<Vec<_>>(),
[sha1, sha256, tail]
);
assert_eq!(hashed.len(), 3);
assert_eq!(hashed[&sha1], 1);
assert_eq!(hashed[&sha256], 2);
assert_eq!(hashed[&tail], 3);
}
}
#[cfg(test)]
thread_local! {
pub(crate) static HASHES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}