use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
use crate::hash::Hash;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObjectType {
Blob,
Tree,
Changeset,
Intent,
}
impl ObjectType {
pub fn code(self) -> u8 {
match self {
ObjectType::Blob => 1,
ObjectType::Tree => 2,
ObjectType::Changeset => 3,
ObjectType::Intent => 4,
}
}
pub fn from_code(code: u8) -> Result<Self> {
match code {
1 => Ok(ObjectType::Blob),
2 => Ok(ObjectType::Tree),
3 => Ok(ObjectType::Changeset),
4 => Ok(ObjectType::Intent),
other => Err(Error::UnknownObjectType(other)),
}
}
}
pub trait Object: Sized {
const TYPE: ObjectType;
fn encode_body(&self) -> Result<Vec<u8>>;
fn decode_body(bytes: &[u8]) -> Result<Self>;
fn id(&self) -> Result<Hash> {
Ok(Hash::of(&self.encode_body()?))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Blob {
pub content: Vec<u8>,
}
impl Blob {
pub fn new(content: impl Into<Vec<u8>>) -> Self {
Self {
content: content.into(),
}
}
}
impl Object for Blob {
const TYPE: ObjectType = ObjectType::Blob;
fn encode_body(&self) -> Result<Vec<u8>> {
Ok(self.content.clone())
}
fn decode_body(bytes: &[u8]) -> Result<Self> {
Ok(Blob {
content: bytes.to_vec(),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EntryKind {
File,
Directory,
Symlink,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TreeEntry {
pub name: String,
pub kind: EntryKind,
pub hash: Hash,
pub mode: u32,
pub size: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Tree {
pub entries: Vec<TreeEntry>,
}
impl Tree {
pub fn new(entries: Vec<TreeEntry>) -> Self {
Self { entries }
}
fn canonical_entries(&self) -> Vec<TreeEntry> {
let mut e = self.entries.clone();
e.sort_by(|a, b| a.name.cmp(&b.name));
e
}
}
impl Object for Tree {
const TYPE: ObjectType = ObjectType::Tree;
fn encode_body(&self) -> Result<Vec<u8>> {
rmp_serde::to_vec(&self.canonical_entries()).map_err(|e| Error::Serialize(e.to_string()))
}
fn decode_body(bytes: &[u8]) -> Result<Self> {
let entries: Vec<TreeEntry> =
rmp_serde::from_slice(bytes).map_err(|e| Error::Deserialize(e.to_string()))?;
Ok(Tree { entries })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blob_id_is_blake3_of_content() {
let b = Blob::new(b"hello".to_vec());
assert_eq!(b.id().unwrap(), Hash::of(b"hello"));
}
#[test]
fn blob_body_roundtrips() {
let b = Blob::new(b"some bytes".to_vec());
let body = b.encode_body().unwrap();
assert_eq!(Blob::decode_body(&body).unwrap(), b);
}
#[test]
fn tree_id_is_order_independent() {
let e1 = TreeEntry {
name: "a.txt".into(),
kind: EntryKind::File,
hash: Hash::of(b"a"),
mode: 0o644,
size: 1,
};
let e2 = TreeEntry {
name: "b.txt".into(),
kind: EntryKind::File,
hash: Hash::of(b"b"),
mode: 0o644,
size: 1,
};
let t1 = Tree::new(vec![e1.clone(), e2.clone()]);
let t2 = Tree::new(vec![e2, e1]);
assert_eq!(t1.id().unwrap(), t2.id().unwrap());
}
#[test]
fn tree_body_roundtrips() {
let t = Tree::new(vec![TreeEntry {
name: "src".into(),
kind: EntryKind::Directory,
hash: Hash::of(b"tree"),
mode: 0o755,
size: 0,
}]);
let body = t.encode_body().unwrap();
assert_eq!(Tree::decode_body(&body).unwrap().entries.len(), 1);
}
#[test]
fn object_type_codes_roundtrip() {
for ty in [ObjectType::Blob, ObjectType::Tree] {
assert_eq!(ObjectType::from_code(ty.code()).unwrap(), ty);
}
assert!(ObjectType::from_code(99).is_err());
}
}