diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..d02e483 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,5 @@ +#[derive(thiserror::Error, Debug)] +pub enum RuntimeError { + #[error("Invalid character found")] + UnexpectedChar, +} diff --git a/src/kind.rs b/src/kind.rs new file mode 100644 index 0000000..9e40ce2 --- /dev/null +++ b/src/kind.rs @@ -0,0 +1,31 @@ +use anyhow::{anyhow, Result}; + +#[derive(Debug)] +pub enum Kind { + Blob, // 100644 or 100755 + Commit, // 160000 + Tree, // 040000 + Symlink, // 120000 +} + +impl Kind { + pub fn from_mode(mode: &str) -> Result { + match mode { + "100644" | "100755" => Ok(Kind::Blob), + "160000" => Ok(Kind::Commit), + "120000" => Ok(Kind::Symlink), + "040000" | "40000" => Ok(Kind::Tree), + + _ => Err(anyhow!(format!("invalid mode: {}", mode))), + } + } + + pub fn string(&self) -> &str { + match self { + Kind::Blob => "blob", + Kind::Commit => "commit", + Kind::Tree => "tree", + Kind::Symlink => "symlink", + } + } +} diff --git a/src/main.rs b/src/main.rs index c789914..e713c7a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,16 @@ -use anyhow::{Context, Error, Result}; +use anyhow::{Error, Result}; use std::env; -use std::io::prelude::*; -use std::path::Path; use std::{fs, path::PathBuf}; use clap::Parser; use clap::Subcommand; +mod error; +mod kind; +mod object; + +use crate::object::read_object; + #[derive(Parser)] #[command(name = "mg", about = "A simple git clone")] struct Cli { @@ -29,57 +33,6 @@ enum Command { }, } -#[derive(thiserror::Error, Debug)] -pub enum RuntimeError { - #[error("Invalid character found")] - UnexpectedChar, -} - -#[derive(Debug)] -enum Kind { - Blob, // 100644 or 100755 - Commit, // 160000 - Tree, // 040000 - Symlink, // 120000 -} - -impl Kind { - fn from_mode(mode: &str) -> Result { - match mode { - "100644" | "100755" => Ok(Kind::Blob), - "160000" => Ok(Kind::Commit), - "120000" => Ok(Kind::Symlink), - "040000" | "40000" => Ok(Kind::Tree), - - _ => Err(anyhow::anyhow!(format!("invalid mode: {}", mode))), - } - } - - fn string(&self) -> &str { - match self { - Kind::Blob => "blob", - Kind::Commit => "commit", - Kind::Tree => "tree", - Kind::Symlink => "symlink", - } - } -} - -#[derive(Debug)] -struct Object { - kind: Kind, - _size: usize, - data: Reader, -} - -#[derive(Debug)] -struct TreeObject { - mode: String, - kind: Kind, - name: String, - hash: [u8; 20], -} - fn default_init_path() -> PathBuf { env::var("REPO_PATH") .map(PathBuf::from) @@ -98,116 +51,6 @@ fn init_repository(path: PathBuf) -> Result { Ok(path) } -fn read_object(path: &Path, object: &str) -> Result> { - let object_path = path - .join(".git") - .join("objects") - .join(&object[..2]) - .join(&object[2..]); - - let fd = fs::File::open(&object_path).context("opening the object")?; - let zfd = flate2::read::ZlibDecoder::new(fd); - let mut buf_reader = std::io::BufReader::new(zfd); - - let mut buf: Vec = Vec::new(); - buf_reader - .read_until(0, &mut buf) - .context("read the object")?; - - match buf.pop() { - Some(0) => {} - Some(_) | None => return Err(RuntimeError::UnexpectedChar.into()), - }; - - let header = String::from_utf8(buf.clone()).context("converting header to utf-8")?; - - let Some((object_type, object_size)) = header.split_once(' ') else { - anyhow::bail!("could not parse object header correctly"); - }; - - let object_type = match object_type { - "blob" => Kind::Blob, - "commit" => Kind::Commit, - "tree" => Kind::Tree, - _ => anyhow::bail!("invalid object type found"), - }; - - let object_size = object_size.parse::()?; - - Ok(Object { - kind: object_type, - _size: object_size, - data: buf_reader, - }) -} - -impl Object { - fn print(&mut self) -> Result<()> { - let mut buf: Vec = Vec::new(); - let mut buf_hash: [u8; 20] = [0; 20]; - - match self.kind { - Kind::Blob | Kind::Commit => { - self.data.read_to_end(&mut buf)?; - println!("{}", String::from_utf8(buf)?); - } - Kind::Tree => { - let mut max_name_len = 0; - let mut entries = Vec::new(); - loop { - let read_bytes_len = self.data.read_until(0, &mut buf)?; - if read_bytes_len == 0 { - break; - } - - let mode_name = buf.clone(); - buf.clear(); - let mut splits = mode_name.splitn(2, |&b| b == b' '); - - let mode = splits - .next() - .ok_or_else(|| anyhow::anyhow!("could not parse mode"))?; - let mode = std::str::from_utf8(mode)?; - let name = splits - .next() - .ok_or_else(|| anyhow::anyhow!("could not parse name"))?; - let name = std::str::from_utf8(name)?; - - self.data.read_exact(&mut buf_hash)?; - - if name.len() > max_name_len { - max_name_len = name.len(); - } - - entries.push(TreeObject { - name: name.to_string(), - kind: Kind::from_mode(mode)?, - mode: mode.to_string(), - hash: buf_hash, - }); - } - - entries.sort_by(|a, b| a.name.cmp(&b.name)); - - for entry in entries { - let hash = hex::encode(entry.hash); - println!( - "{:0>6} {} {} {:name_len$}", - entry.mode, - entry.kind.string(), - hash, - entry.name, - name_len = max_name_len - ); - } - } - _ => unimplemented!(), - } - - Ok(()) - } -} - fn main() -> Result<(), Error> { let cli = Cli::parse(); let path = default_init_path(); @@ -218,7 +61,7 @@ fn main() -> Result<(), Error> { Err(e) => eprintln!("Failed to initialize repository: {}", e), }, Command::CatFile { hash } => match read_object(&path, &hash) { - Ok(mut obj) => obj.print()?, + Ok(mut obj) => println!("{}", obj.to_string()?), Err(e) => eprintln!("Failed to read object: {}", e), }, } diff --git a/src/object.rs b/src/object.rs new file mode 100644 index 0000000..7af1952 --- /dev/null +++ b/src/object.rs @@ -0,0 +1,133 @@ +use anyhow::{Context, Result}; +use std::{fs::File, io::BufRead, path::Path}; + +use crate::{error::RuntimeError, kind::Kind}; + +#[derive(Debug)] +pub struct Object { + kind: Kind, + _size: usize, + data: Reader, +} + +#[derive(Debug)] +struct TreeObject { + mode: String, + kind: Kind, + name: String, + hash: [u8; 20], +} + +pub fn read_object(path: &Path, object: &str) -> Result> { + let object_path = path + .join(".git") + .join("objects") + .join(&object[..2]) + .join(&object[2..]); + + let fd = File::open(&object_path).context("opening the object")?; + let zfd = flate2::read::ZlibDecoder::new(fd); + let mut buf_reader = std::io::BufReader::new(zfd); + + let mut buf: Vec = Vec::new(); + buf_reader + .read_until(0, &mut buf) + .context("read the object")?; + + match buf.pop() { + Some(0) => {} + Some(_) | None => return Err(RuntimeError::UnexpectedChar.into()), + }; + + let header = String::from_utf8(buf.clone()).context("converting header to utf-8")?; + + let Some((object_type, object_size)) = header.split_once(' ') else { + anyhow::bail!("could not parse object header correctly"); + }; + + let object_type = match object_type { + "blob" => Kind::Blob, + "commit" => Kind::Commit, + "tree" => Kind::Tree, + _ => anyhow::bail!("invalid object type found"), + }; + + let object_size = object_size.parse::()?; + + Ok(Object { + kind: object_type, + _size: object_size, + data: buf_reader, + }) +} + +impl Object { + pub fn to_string(&mut self) -> Result { + let mut buf: Vec = Vec::new(); + let mut buf_hash: [u8; 20] = [0; 20]; + + let res = match self.kind { + Kind::Blob | Kind::Commit => { + self.data.read_to_end(&mut buf)?; + String::from_utf8(buf)? + } + Kind::Tree => { + let mut max_name_len = 0; + let mut entries = Vec::new(); + loop { + let read_bytes_len = self.data.read_until(0, &mut buf)?; + if read_bytes_len == 0 { + break; + } + + let mode_name = buf.clone(); + buf.clear(); + let mut splits = mode_name.splitn(2, |&b| b == b' '); + + let mode = splits + .next() + .ok_or_else(|| anyhow::anyhow!("could not parse mode"))?; + let mode = std::str::from_utf8(mode)?; + let name = splits + .next() + .ok_or_else(|| anyhow::anyhow!("could not parse name"))?; + let name = std::str::from_utf8(name)?; + + self.data.read_exact(&mut buf_hash)?; + + if name.len() > max_name_len { + max_name_len = name.len(); + } + + entries.push(TreeObject { + name: name.to_string(), + kind: Kind::from_mode(mode)?, + mode: mode.to_string(), + hash: buf_hash, + }); + } + + entries.sort_by(|a, b| a.name.cmp(&b.name)); + + entries + .iter() + .map(|entry| { + let hash = hex::encode(entry.hash); + format!( + "{:0>6} {} {} {:name_len$}", + entry.mode, + entry.kind.string(), + hash, + entry.name, + name_len = max_name_len + ) + }) + .collect::>() + .join("\n") + } + _ => unimplemented!(), + }; + + Ok(res) + } +}