use anyhow::Result; use anyhow::bail; use sha1::{Digest, Sha1}; use std::fs; use std::{ffi::CStr, fmt::Display, fs::File, io::prelude::*, os::unix::fs::PermissionsExt}; use crate::git_fs::get_git_root; #[derive(Clone, Debug, PartialEq)] pub enum ObjectType { Regular, SymLink, GitLink, } impl Display for ObjectType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self { Self::Regular => write!(f, "Regular"), Self::SymLink => write!(f, "Symlink"), Self::GitLink => write!(f, "Gitlink"), } } } #[derive(Clone, Debug, PartialEq)] pub struct IndexEntry { object_type: ObjectType, permissions: u16, hash: [u8; 20], name: String, } impl IndexEntry { fn from_bytes(bytes: &[u8]) -> Result<(Self, usize)> { let mode = &bytes[24..28]; let ot_bin = mode[2] >> 4; let object_type = match ot_bin { 0b1000 => ObjectType::Regular, 0b1010 => ObjectType::SymLink, 0b1110 => ObjectType::GitLink, _ => bail!("Invalid object type: {}", ot_bin), }; let permissions = u16::from_be_bytes(mode[2..4].try_into()?) & !0xFE00; match (permissions, &object_type) { (0, ObjectType::GitLink) => (), (0, ObjectType::SymLink) => (), (0o755, ObjectType::Regular) => (), (0o644, ObjectType::Regular) => (), _ => bail!( "Invalid permissions (0o{:o}) for type {}", permissions, object_type ), }; let hash: [u8; 20] = bytes[40..60].try_into()?; let cname = CStr::from_bytes_until_nul(&bytes[62..])?; let name = String::from(cname.to_str()?); let entry_size = usize::div_ceil(62 + cname.count_bytes() + 1, 8) * 8; Ok(( IndexEntry { object_type, permissions, hash, name, }, entry_size, )) } fn to_bytes(&self) -> Result> { let mut bytes = Vec::new(); // ctime bytes.extend([0, 0, 0, 0]); // ctime nano bytes.extend([0, 0, 0, 0]); // mtime bytes.extend([0, 0, 0, 0]); // mtime nano bytes.extend([0, 0, 0, 0]); // dev bytes.extend([0, 0, 0, 0]); // ino bytes.extend([0, 0, 0, 0]); // mode let ot_bin: u16 = match self.object_type { ObjectType::Regular => 0b1000, ObjectType::SymLink => 0b1010, ObjectType::GitLink => 0b1110, }; let perms: u16 = self.permissions | (ot_bin << 12); bytes.extend([0, 0]); bytes.extend(perms.to_be_bytes()); // uid bytes.extend([0, 0, 0, 0]); // gid bytes.extend([0, 0, 0, 0]); // file size bytes.extend([0, 0, 0, 0]); // object name bytes.extend(self.hash); // flags let flags = u16::min(0xFFF, self.name.len() as u16); bytes.extend(flags.to_be_bytes()); // entry path name bytes.extend(self.name.as_bytes()); let padding = ((bytes.len() + 1).div_ceil(8) * 8) - bytes.len(); bytes.extend(vec![0; padding]); Ok(bytes) } } #[derive(Debug, PartialEq)] pub struct Index { version: u32, entries: Vec, } impl Default for Index { fn default() -> Self { Index { version: 2, entries: Vec::new(), } } } impl Index { pub fn load() -> Result { let path = get_git_root()?.join("index"); if !path.exists() { return Ok(Index { version: 2u32, entries: Vec::new(), }); } let mut file = File::open(&path)?; let mut content: Vec = Vec::new(); file.read_to_end(&mut content)?; Index::from_bytes(content) } pub fn save(&self) -> Result<()> { let path = get_git_root()?.join("index"); let mut file = File::create(&path)?; file.write_all(&self.to_bytes()?)?; Ok(()) } pub fn add_file(&mut self, name: String, hash: [u8; 20]) -> Result<()> { let metadata = fs::metadata(&name)?; if metadata.is_dir() { bail!("Cannot add a directory to index") }; let object_type = if metadata.is_symlink() { ObjectType::SymLink } else { ObjectType::Regular }; let permissions = (metadata.permissions().mode() as u16) & !0xFE00; let entry = IndexEntry { name, hash, object_type, permissions, }; self.insert_entry(entry); Ok(()) } pub fn remove_file(&mut self, name: String) -> Result<()> { let entry = IndexEntry { name: name.clone(), hash: [0u8; 20], object_type: ObjectType::Regular, permissions: 0, }; match self .entries .binary_search_by_key(&entry.name, |a| a.name.clone()) { Ok(pos) => self.entries.remove(pos), Err(_) => bail!("No file {} in index", name), }; Ok(()) } fn insert_entry(&mut self, entry: IndexEntry) { match self .entries .binary_search_by_key(&entry.name, |e| e.name.clone()) { Ok(pos) => self.entries[pos] = entry, Err(pos) => self.entries.insert(pos, entry), } } fn from_bytes(bytes: Vec) -> Result { match &bytes[0..4] { b"DIRC" => (), _ => bail!("Invalid index signature"), }; let version = u32::from_be_bytes(bytes[4..8].try_into()?); let count = u32::from_be_bytes(bytes[8..12].try_into()?); let mut entries: Vec = Vec::with_capacity(usize::try_from(count)?); let mut offset = 12; for _i in 0..count { let (entry, entry_size) = IndexEntry::from_bytes(&bytes[offset..])?; offset += entry_size; entries.push(entry); } let mut hasher = Sha1::new(); hasher.update(&bytes[..offset]); let hash = hasher.finalize(); if bytes[offset..] != *hash { bail!("Index does not match its checksum") } Ok(Index { version, entries }) } fn to_bytes(&self) -> Result> { let mut bytes: Vec = Vec::new(); bytes.extend("DIRC".as_bytes()); bytes.extend(self.version.to_be_bytes()); let count: u32 = self.entries.len() as u32; bytes.extend(count.to_be_bytes()); for entry in &self.entries { bytes.extend(entry.to_bytes()?); } let mut hasher = Sha1::new(); hasher.update(&bytes); let hash = hasher.finalize(); bytes.extend(hash); Ok(bytes) } } #[cfg(test)] mod tests { use super::*; #[test] fn index_entry_to_bytes() { let entry1 = IndexEntry { object_type: ObjectType::Regular, permissions: 0o644u16, hash: [0x19u8; 20], name: String::from("src/git_fs/head.rs"), }; let expected1: Vec = vec![ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x00, 0x12, 0x73, 0x72, 0x63, 0x2f, 0x67, 0x69, 0x74, 0x5f, 0x66, 0x73, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x2e, 0x72, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; let entry2 = IndexEntry { object_type: ObjectType::GitLink, permissions: 0, hash: [0x19u8; 20], name: String::from("src/git_fs/head.r"), }; let expected2: Vec = vec![ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x00, 0x11, 0x73, 0x72, 0x63, 0x2f, 0x67, 0x69, 0x74, 0x5f, 0x66, 0x73, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x2e, 0x72, 0x00, ]; let entry3 = IndexEntry { object_type: ObjectType::SymLink, permissions: 0, hash: [0x19u8; 20], name: String::from("src/git_fs/head.rst"), }; let expected3: Vec = vec![ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x00, 0x13, 0x73, 0x72, 0x63, 0x2f, 0x67, 0x69, 0x74, 0x5f, 0x66, 0x73, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x2e, 0x72, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; assert_eq!(entry1.to_bytes().unwrap(), expected1); assert_eq!(entry2.to_bytes().unwrap(), expected2); assert_eq!(entry3.to_bytes().unwrap(), expected3); } #[test] fn index_entry_from_bytes() { let expected1 = IndexEntry { object_type: ObjectType::Regular, permissions: 0o644u16, hash: [0x19u8; 20], name: String::from("src/git_fs/head.rs"), }; let (entry1, len1) = IndexEntry::from_bytes(&vec![ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, 0xa4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x00, 0x12, 0x73, 0x72, 0x63, 0x2f, 0x67, 0x69, 0x74, 0x5f, 0x66, 0x73, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x2e, 0x72, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]) .unwrap(); let expected2 = IndexEntry { object_type: ObjectType::GitLink, permissions: 0, hash: [0x19u8; 20], name: String::from("src/git_fs/head.r"), }; let (entry2, len2) = IndexEntry::from_bytes(&vec![ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x00, 0x11, 0x73, 0x72, 0x63, 0x2f, 0x67, 0x69, 0x74, 0x5f, 0x66, 0x73, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x2e, 0x72, 0x00, ]) .unwrap(); let expected3 = IndexEntry { object_type: ObjectType::SymLink, permissions: 0, hash: [0x19u8; 20], name: String::from("src/git_fs/head.rst"), }; let (entry3, len3) = IndexEntry::from_bytes(&vec![ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x00, 0x13, 0x73, 0x72, 0x63, 0x2f, 0x67, 0x69, 0x74, 0x5f, 0x66, 0x73, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x2e, 0x72, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]) .unwrap(); assert_eq!(entry1, expected1); assert_eq!(entry2, expected2); assert_eq!(entry3, expected3); assert_eq!(len1, 88); assert_eq!(len2, 80); assert_eq!(len3, 88); } #[test] fn index_insert_entry() { let mut index = Index::default(); let entry1 = IndexEntry { object_type: ObjectType::Regular, permissions: 0o644u16, hash: [0x19u8; 20], name: String::from("a"), }; let entry2 = IndexEntry { object_type: ObjectType::Regular, permissions: 0o644u16, hash: [0x19u8; 20], name: String::from("b"), }; let entry3 = IndexEntry { object_type: ObjectType::SymLink, permissions: 0, hash: [0x19u8; 20], name: String::from(".hello"), }; let entry4 = IndexEntry { object_type: ObjectType::Regular, permissions: 0o644u16, hash: [0x20u8; 20], name: String::from("b"), }; index.insert_entry(entry1.clone()); index.insert_entry(entry2.clone()); index.insert_entry(entry3.clone()); index.insert_entry(entry4.clone()); let expected = Index { entries: vec![entry3, entry1, entry4], ..Default::default() }; assert_eq!(index, expected); } #[test] fn index_remove_entry() { let entry1 = IndexEntry { object_type: ObjectType::Regular, permissions: 0o644u16, hash: [0x19u8; 20], name: String::from("a"), }; let entry2 = IndexEntry { object_type: ObjectType::Regular, permissions: 0o644u16, hash: [0x19u8; 20], name: String::from("b"), }; let entry3 = IndexEntry { object_type: ObjectType::SymLink, permissions: 0, hash: [0x19u8; 20], name: String::from(".hello"), }; let mut index = Index { entries: vec![entry3.clone(), entry1, entry2.clone()], ..Default::default() }; index.remove_file(String::from("a")).unwrap(); let expected = Index { entries: vec![entry3, entry2], ..Default::default() }; assert_eq!(index, expected); } }