use std::{fmt::Display, str::FromStr};
use bstr::ByteSlice;
use chrono::Offset;
use crate::errors::GitError;
#[derive(
PartialEq,
Eq,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
rkyv::Archive,
rkyv::Serialize,
rkyv::Deserialize,
)]
pub enum SignatureType {
Author,
Committer,
Tagger,
}
impl Display for SignatureType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
SignatureType::Author => write!(f, "author"),
SignatureType::Committer => write!(f, "committer"),
SignatureType::Tagger => write!(f, "tagger"),
}
}
}
impl FromStr for SignatureType {
type Err = GitError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"author" => Ok(SignatureType::Author),
"committer" => Ok(SignatureType::Committer),
"tagger" => Ok(SignatureType::Tagger),
_ => Err(GitError::InvalidSignatureType(s.to_string())),
}
}
}
impl SignatureType {
pub fn from_data(data: Vec<u8>) -> Result<Self, GitError> {
let s = String::from_utf8(data.to_vec())
.map_err(|e| GitError::ConversionError(e.to_string()))?;
SignatureType::from_str(s.as_str())
}
pub fn to_bytes(&self) -> Vec<u8> {
match self {
SignatureType::Author => "author".to_string().into_bytes(),
SignatureType::Committer => "committer".to_string().into_bytes(),
SignatureType::Tagger => "tagger".to_string().into_bytes(),
}
}
}
#[derive(
PartialEq,
Eq,
Debug,
Clone,
serde::Serialize,
serde::Deserialize,
rkyv::Archive,
rkyv::Serialize,
rkyv::Deserialize,
)]
pub struct Signature {
pub signature_type: SignatureType,
pub name: String,
pub email: String,
pub timestamp: usize,
pub timezone: String,
}
impl Display for Signature {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
writeln!(f, "{} <{}>", self.name, self.email).unwrap();
let date =
chrono::DateTime::<chrono::Utc>::from_timestamp(self.timestamp as i64, 0).unwrap();
writeln!(f, "Date: {} {}", date, self.timezone)
}
}
impl Signature {
pub fn from_data(data: Vec<u8>) -> Result<Signature, GitError> {
let mut sign = data;
let name_start = sign.find_byte(0x20).unwrap();
let signature_type = SignatureType::from_data(sign[..name_start].to_vec())?;
let (name, email) = {
let email_start = sign.find_byte(0x3C).unwrap();
let email_end = sign.find_byte(0x3E).unwrap();
unsafe {
(
sign[name_start + 1..email_start - 1]
.to_str_unchecked()
.to_string(),
sign[email_start + 1..email_end]
.to_str_unchecked()
.to_string(),
)
}
};
sign = sign[sign.find_byte(0x3E).unwrap() + 2..].to_vec();
let timestamp_split = sign.find_byte(0x20).unwrap();
let timestamp = unsafe {
sign[0..timestamp_split]
.to_str_unchecked()
.parse::<usize>()
.unwrap()
};
let timezone = unsafe { sign[timestamp_split + 1..].to_str_unchecked().to_string() };
Ok(Signature {
signature_type,
name,
email,
timestamp,
timezone,
})
}
pub fn to_data(&self) -> Result<Vec<u8>, GitError> {
let mut sign = Vec::new();
sign.extend_from_slice(&self.signature_type.to_bytes());
sign.extend_from_slice(&[0x20]);
sign.extend_from_slice(self.name.as_bytes());
sign.extend_from_slice(&[0x20]);
sign.extend_from_slice(format!("<{}>", self.email).as_bytes());
sign.extend_from_slice(&[0x20]);
sign.extend_from_slice(self.timestamp.to_string().as_bytes());
sign.extend_from_slice(&[0x20]);
sign.extend_from_slice(self.timezone.as_bytes());
Ok(sign)
}
pub fn new(sign_type: SignatureType, author: String, email: String) -> Signature {
let local_time = chrono::Local::now();
let offset = local_time.offset().fix().local_minus_utc();
let hours = offset / 60 / 60;
let minutes = offset / 60 % 60;
let offset_str = format!("{hours:+03}{minutes:02}");
Signature {
signature_type: sign_type, name: author, email, timestamp: chrono::Utc::now().timestamp() as usize, timezone: offset_str, }
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use chrono::DateTime;
use crate::internal::object::signature::{Signature, SignatureType};
#[test]
fn test_signature_type_from_str() {
assert_eq!(
SignatureType::from_str("author").unwrap(),
SignatureType::Author
);
assert_eq!(
SignatureType::from_str("committer").unwrap(),
SignatureType::Committer
);
}
#[test]
fn test_signature_type_from_data() {
assert_eq!(
SignatureType::from_data("author".to_string().into_bytes()).unwrap(),
SignatureType::Author
);
assert_eq!(
SignatureType::from_data("committer".to_string().into_bytes()).unwrap(),
SignatureType::Committer
);
}
#[test]
fn test_signature_type_to_bytes() {
assert_eq!(
SignatureType::Author.to_bytes(),
"author".to_string().into_bytes()
);
assert_eq!(
SignatureType::Committer.to_bytes(),
"committer".to_string().into_bytes()
);
}
#[test]
fn test_signature_new_from_data() {
let sign = Signature::from_data(
"author Quanyi Ma <eli@patch.sh> 1678101573 +0800"
.to_string()
.into_bytes(),
)
.unwrap();
assert_eq!(sign.signature_type, SignatureType::Author);
assert_eq!(sign.name, "Quanyi Ma");
assert_eq!(sign.email, "eli@patch.sh");
assert_eq!(sign.timestamp, 1678101573);
assert_eq!(sign.timezone, "+0800");
}
#[test]
fn test_signature_to_data() {
let sign = Signature::from_data(
"committer Quanyi Ma <eli@patch.sh> 1678101573 +0800"
.to_string()
.into_bytes(),
)
.unwrap();
let dest = sign.to_data().unwrap();
assert_eq!(
dest,
"committer Quanyi Ma <eli@patch.sh> 1678101573 +0800"
.to_string()
.into_bytes()
);
}
#[test]
fn test_signature_with_time() {
let sign = Signature::new(
SignatureType::Author,
"MEGA".to_owned(),
"admin@mega.com".to_owned(),
);
assert_eq!(sign.signature_type, SignatureType::Author);
assert_eq!(sign.name, "MEGA");
assert_eq!(sign.email, "admin@mega.com");
let naive_datetime = DateTime::from_timestamp(sign.timestamp as i64, 0).unwrap();
println!("Formatted DateTime: {}", naive_datetime.naive_local());
}
}