pub mod commit;
pub mod signature;
pub mod tag;
pub mod tree;
pub use commit::Commit;
pub use signature::Signature;
pub use tag::Tag;
pub use tree::{FileMode, Tree, TreeEntry};
use alloc::vec::Vec;
use crate::error::{Error, Result};
use crate::hash::loose_header;
use crate::oid::HashAlgo;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ObjectType {
Blob,
Tree,
Commit,
Tag,
}
impl ObjectType {
pub const fn as_str(self) -> &'static str {
match self {
ObjectType::Blob => "blob",
ObjectType::Tree => "tree",
ObjectType::Commit => "commit",
ObjectType::Tag => "tag",
}
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Result<Self> {
Ok(match s {
"blob" => ObjectType::Blob,
"tree" => ObjectType::Tree,
"commit" => ObjectType::Commit,
"tag" => ObjectType::Tag,
other => {
use alloc::format;
return Err(Error::Parse(format!("unknown object type {other:?}")));
}
})
}
}
impl core::fmt::Display for ObjectType {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Object {
Blob(Vec<u8>),
Tree(Tree),
Commit(Commit),
Tag(Tag),
}
impl Object {
pub fn object_type(&self) -> ObjectType {
match self {
Object::Blob(_) => ObjectType::Blob,
Object::Tree(_) => ObjectType::Tree,
Object::Commit(_) => ObjectType::Commit,
Object::Tag(_) => ObjectType::Tag,
}
}
pub fn parse(algo: HashAlgo, ty: ObjectType, payload: &[u8]) -> Result<Self> {
Ok(match ty {
ObjectType::Blob => Object::Blob(payload.to_vec()),
ObjectType::Tree => Object::Tree(Tree::parse(algo, payload)?),
ObjectType::Commit => Object::Commit(Commit::parse(algo, payload)?),
ObjectType::Tag => Object::Tag(Tag::parse(algo, payload)?),
})
}
pub fn payload(&self) -> Vec<u8> {
match self {
Object::Blob(b) => b.clone(),
Object::Tree(t) => t.serialize(),
Object::Commit(c) => c.serialize(),
Object::Tag(t) => t.serialize(),
}
}
}
pub fn parse_loose(bytes: &[u8]) -> Result<(ObjectType, &[u8])> {
let space = bytes
.iter()
.position(|&b| b == b' ')
.ok_or_else(|| Error::Parse("loose object: missing space in header".into()))?;
let nul = bytes
.iter()
.position(|&b| b == 0)
.ok_or_else(|| Error::Parse("loose object: missing NUL in header".into()))?;
if nul < space {
return Err(Error::Parse("loose object: malformed header".into()));
}
let ty = core::str::from_utf8(&bytes[..space])
.map_err(|_| Error::Parse("loose object: non-utf8 type".into()))?;
let ty = ObjectType::from_str(ty)?;
let size_str = core::str::from_utf8(&bytes[space + 1..nul])
.map_err(|_| Error::Parse("loose object: non-utf8 size".into()))?;
let size: usize = size_str
.parse()
.map_err(|_| Error::Parse("loose object: invalid size".into()))?;
let payload = &bytes[nul + 1..];
if payload.len() != size {
use alloc::format;
return Err(Error::Parse(format!(
"loose object: declared size {size} != payload length {}",
payload.len()
)));
}
Ok((ty, payload))
}
pub fn serialize_loose(ty: ObjectType, payload: &[u8]) -> Vec<u8> {
let mut out = loose_header(ty, payload.len());
out.extend_from_slice(payload);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loose_roundtrip() {
let bytes = serialize_loose(ObjectType::Blob, b"hello\n");
assert_eq!(&bytes[..5], b"blob ");
let (ty, payload) = parse_loose(&bytes).unwrap();
assert_eq!(ty, ObjectType::Blob);
assert_eq!(payload, b"hello\n");
}
#[test]
fn rejects_size_mismatch() {
let mut bytes = serialize_loose(ObjectType::Blob, b"hello\n");
bytes.push(b'!'); assert!(parse_loose(&bytes).is_err());
}
#[test]
fn object_parse_blob() {
let o = Object::parse(HashAlgo::Sha1, ObjectType::Blob, b"xyz").unwrap();
assert_eq!(o.object_type(), ObjectType::Blob);
assert_eq!(o.payload(), b"xyz");
}
}