Index parsing and gitignore files
This commit is contained in:
+402
-37
@@ -1,56 +1,421 @@
|
||||
use std::{
|
||||
io::{
|
||||
self,
|
||||
Error,
|
||||
},
|
||||
};
|
||||
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_DIR;
|
||||
use crate::git_fs::get_git_root;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[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 {
|
||||
file_size: u32,
|
||||
object_type: ObjectType,
|
||||
permissions: u16,
|
||||
hash: [u8; 20],
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl IndexEntry {
|
||||
fn from_bytes(bytes: Vec<u8>) -> io::Result<Self> {
|
||||
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
|
||||
),
|
||||
};
|
||||
|
||||
return Err(Error::other("Not implemented"))
|
||||
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(), 8) * 8;
|
||||
|
||||
Ok((
|
||||
IndexEntry {
|
||||
object_type,
|
||||
permissions,
|
||||
hash,
|
||||
name,
|
||||
},
|
||||
entry_size,
|
||||
))
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> Result<Vec<u8>> {
|
||||
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)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Index {
|
||||
version: u32,
|
||||
entries: Vec<IndexEntry>,
|
||||
}
|
||||
|
||||
impl Index {
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> io::Result<Self> {
|
||||
let magic: [u8; 4] = match bytes.first_chunk() {
|
||||
None => return Err(Error::other("Error parsing index")),
|
||||
Some(magic) => *magic,
|
||||
};
|
||||
|
||||
match str::from_utf8(&magic) {
|
||||
Ok("DIRC") => (),
|
||||
_ => return Err(Error::other("Invalid index"))
|
||||
};
|
||||
|
||||
let version_bytes = <[u8; 4]>::try_from(&bytes.as_slice()[4..8]).unwrap();
|
||||
let version = u32::from_be_bytes(version_bytes);
|
||||
|
||||
let count_bytes = <[u8; 4]>::try_from(&bytes.as_slice()[8..12]).unwrap();
|
||||
let count = u32::from_be_bytes(count_bytes);
|
||||
|
||||
let entries: Vec<IndexEntry> = Vec::with_capacity(usize::try_from(count).unwrap());
|
||||
|
||||
for i in 0..=count {
|
||||
|
||||
};
|
||||
|
||||
return Ok(Index { version, entries});
|
||||
impl Default for Index {
|
||||
fn default() -> Self {
|
||||
Index {
|
||||
version: 2,
|
||||
entries: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Index {
|
||||
pub fn load() -> Result<Self> {
|
||||
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<u8> = 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(())
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
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 from_bytes(bytes: Vec<u8>) -> Result<Self> {
|
||||
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<IndexEntry> = 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<Vec<u8>> {
|
||||
let mut bytes: Vec<u8> = 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<u8> = 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<u8> = 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<u8> = 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, _) = 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, _) = 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, _) = 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);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user