use std::fmt::Display;
use colored::Colorize;
use encoding_rs::GBK;
use crate::{
errors::GitError,
hash::{ObjectHash, get_hash_kind},
internal::object::{ObjectTrait, ObjectType},
};
#[derive(
PartialEq,
Eq,
Debug,
Clone,
Copy,
serde::Serialize,
serde::Deserialize,
Hash,
rkyv::Archive,
rkyv::Serialize,
rkyv::Deserialize,
)]
pub enum TreeItemMode {
Blob,
BlobExecutable,
Tree,
Commit,
Link,
}
impl Display for TreeItemMode {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let _print = match *self {
TreeItemMode::Blob => "blob",
TreeItemMode::BlobExecutable => "blob executable",
TreeItemMode::Tree => "tree",
TreeItemMode::Commit => "commit",
TreeItemMode::Link => "link",
};
write!(f, "{}", String::from(_print).blue())
}
}
impl TreeItemMode {
pub fn tree_item_type_from_bytes(mode: &[u8]) -> Result<TreeItemMode, GitError> {
Ok(match mode {
b"40000" => TreeItemMode::Tree,
b"100644" => TreeItemMode::Blob,
b"100755" => TreeItemMode::BlobExecutable,
b"120000" => TreeItemMode::Link,
b"160000" => TreeItemMode::Commit,
b"100664" => TreeItemMode::Blob,
b"100640" => TreeItemMode::Blob,
_ => {
return Err(GitError::InvalidTreeItem(
String::from_utf8(mode.to_vec()).unwrap(),
));
}
})
}
pub fn to_bytes(self) -> &'static [u8] {
match self {
TreeItemMode::Blob => b"100644",
TreeItemMode::BlobExecutable => b"100755",
TreeItemMode::Link => b"120000",
TreeItemMode::Tree => b"40000",
TreeItemMode::Commit => b"160000",
}
}
}
#[derive(
PartialEq,
Eq,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
Hash,
rkyv::Archive,
rkyv::Serialize,
rkyv::Deserialize,
)]
pub struct TreeItem {
pub mode: TreeItemMode,
pub id: ObjectHash,
pub name: String,
}
impl Display for TreeItem {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{} {} {}",
self.mode,
self.name,
self.id.to_string().blue()
)
}
}
impl TreeItem {
pub fn new(mode: TreeItemMode, id: ObjectHash, name: String) -> Self {
TreeItem { mode, id, name }
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, GitError> {
let mut parts = bytes.splitn(2, |b| *b == b' ');
let mode = parts.next().unwrap();
let rest = parts.next().unwrap();
let mut parts = rest.splitn(2, |b| *b == b'\0');
let raw_name = parts.next().unwrap();
let id = parts.next().unwrap();
let name = if String::from_utf8(raw_name.to_vec()).is_ok() {
String::from_utf8(raw_name.to_vec()).unwrap()
} else {
let (decoded, _, had_errors) = GBK.decode(raw_name);
if had_errors {
return Err(GitError::InvalidTreeItem(format!(
"Unsupported raw format: {raw_name:?}"
)));
} else {
decoded.to_string()
}
};
Ok(TreeItem {
mode: TreeItemMode::tree_item_type_from_bytes(mode)?,
id: ObjectHash::from_bytes(id).unwrap(),
name,
})
}
pub fn to_data(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(self.mode.to_bytes());
bytes.push(b' ');
bytes.extend_from_slice(self.name.as_bytes());
bytes.push(b'\0');
bytes.extend_from_slice(&self.id.to_data());
bytes
}
pub fn is_tree(&self) -> bool {
self.mode == TreeItemMode::Tree
}
pub fn is_blob(&self) -> bool {
self.mode == TreeItemMode::Blob
}
}
#[derive(
Eq,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
rkyv::Archive,
rkyv::Serialize,
rkyv::Deserialize,
)]
pub struct Tree {
pub id: ObjectHash,
pub tree_items: Vec<TreeItem>,
}
impl PartialEq for Tree {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Display for Tree {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
writeln!(f, "Tree: {}", self.id.to_string().blue())?;
for item in &self.tree_items {
writeln!(f, "{item}")?;
}
Ok(())
}
}
impl Tree {
pub fn from_tree_items(tree_items: Vec<TreeItem>) -> Result<Self, GitError> {
if tree_items.is_empty() {
return Err(GitError::EmptyTreeItems(
"When export tree object to meta, the items is empty"
.parse()
.unwrap(),
));
}
let mut data = Vec::new();
for item in &tree_items {
data.extend_from_slice(item.to_data().as_slice());
}
Ok(Tree {
id: ObjectHash::from_type_and_data(ObjectType::Tree, &data),
tree_items,
})
}
pub fn rehash(&mut self) {
let mut data = Vec::new();
for item in &self.tree_items {
data.extend_from_slice(item.to_data().as_slice());
}
self.id = ObjectHash::from_type_and_data(ObjectType::Tree, &data);
}
}
impl TryFrom<&[u8]> for Tree {
type Error = GitError;
fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
let h = ObjectHash::from_type_and_data(ObjectType::Tree, data);
Tree::from_bytes(data, h)
}
}
impl ObjectTrait for Tree {
fn from_bytes(data: &[u8], hash: ObjectHash) -> Result<Self, GitError>
where
Self: Sized,
{
let mut tree_items = Vec::new();
let mut i = 0;
while i < data.len() {
if let Some(index) = memchr::memchr(0x00, &data[i..]) {
let next = i + index + get_hash_kind().size() + 1; if next > data.len() {
return Err(GitError::InvalidTreeObject);
} let item_data = &data[i..next];
let tree_item = TreeItem::from_bytes(item_data)?;
tree_items.push(tree_item);
i = next;
} else {
return Err(GitError::InvalidTreeObject);
}
}
Ok(Tree {
id: hash,
tree_items,
})
}
fn get_type(&self) -> ObjectType {
ObjectType::Tree
}
fn get_size(&self) -> usize {
self.to_data().map(|data| data.len()).unwrap_or(0)
}
fn to_data(&self) -> Result<Vec<u8>, GitError> {
let mut data: Vec<u8> = Vec::new();
for item in &self.tree_items {
data.extend_from_slice(item.to_data().as_slice());
}
Ok(data)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use crate::{
hash::{HashKind, ObjectHash, set_hash_kind_for_test},
internal::object::tree::{Tree, TreeItem, TreeItemMode},
};
fn tree_item_round_trip(kind: HashKind, id_hex: &str) {
let _guard = set_hash_kind_for_test(kind);
let item = TreeItem::new(
TreeItemMode::Blob,
ObjectHash::from_str(id_hex).unwrap(),
"hello-world".to_string(),
);
let bytes = item.to_data();
let parsed = TreeItem::from_bytes(&bytes).unwrap();
assert_eq!(parsed.mode, TreeItemMode::Blob);
assert_eq!(parsed.id, item.id);
assert_eq!(parsed.name, item.name);
}
#[test]
fn tree_item_round_trip_sha1() {
tree_item_round_trip(HashKind::Sha1, "8ab686eafeb1f44702738c8b0f24f2567c36da6d");
}
#[test]
fn tree_item_round_trip_sha256() {
tree_item_round_trip(
HashKind::Sha256,
"2cf8d83d9ee29543b34a87727421fdecb7e3f3a183d337639025de576db9ebb4",
);
}
fn tree_round_trip(kind: HashKind, items: Vec<(&str, &str)>, expected_id: &str) {
let _guard = set_hash_kind_for_test(kind);
let tree_items = items
.into_iter()
.map(|(name, id_hex)| {
TreeItem::new(
TreeItemMode::Blob,
ObjectHash::from_str(id_hex).unwrap(),
name.to_string(),
)
})
.collect::<Vec<_>>();
let tree = Tree::from_tree_items(tree_items).unwrap();
assert_eq!(tree.id.to_string(), expected_id);
}
#[test]
fn tree_from_items_sha1() {
tree_round_trip(
HashKind::Sha1,
vec![("hello-world", "17288789afffb273c8c394bc65e87d899b92897b")],
"cf99336fa61439a2f074c7e6de1c1a05579550e2",
);
}
#[test]
fn tree_from_items_sha256() {
tree_round_trip(
HashKind::Sha256,
vec![
(
"a.txt",
"2cf8d83d9ee29543b34a87727421fdecb7e3f3a183d337639025de576db9ebb4",
),
(
"b.txt",
"fc2593998f8e1dec9c3a8be11557888134dad90ef5c7a2d6236ed75534c7698e",
),
(
"c.txt",
"21513dcb4d6f9eb247db3b4c52158395d94f809cbaa2630bd2a7a474d9b39fab",
),
(
"hello-world",
"2cf8d83d9ee29543b34a87727421fdecb7e3f3a183d337639025de576db9ebb4",
),
(
"message.txt",
"9ba9ae56288652bf32f074f922e37d3e95df8920b3cdfc053309595b8f86cbc6",
),
],
"d712a36aadfb47cabc7aaa90cf9e515773ba3bfc1fe3783730b387ce15c49261",
);
}
}