//! Index file at `.levcs/index`. Format from ยง2.5. //! //! magic 4 bytes "LVIX" //! version 4 bytes format version (1) //! entry_count 4 bytes LE uint32 //! entries variable sequence of index entries //! //! Each entry: //! path_len 2 bytes //! path N bytes (UTF-8, /-separated, repository-relative) //! blob_hash 32 bytes //! mode 1 byte //! flags 1 byte (bit 0 tracked, bit 1 cached, bit 2 conflicted) //! mtime 8 bytes i64 LE microseconds since UNIX epoch //! size 8 bytes u64 LE use std::fs; use std::path::PathBuf; use byteorder::{ByteOrder, LittleEndian}; use crate::error::{Error, IoExt, Result}; use crate::hash::ObjectId; pub const INDEX_MAGIC: [u8; 4] = *b"LVIX"; pub const INDEX_VERSION: u32 = 1; #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct IndexEntryFlags(pub u8); impl IndexEntryFlags { pub const TRACKED: IndexEntryFlags = IndexEntryFlags(0b001); pub const CACHED: IndexEntryFlags = IndexEntryFlags(0b010); pub const CONFLICTED: IndexEntryFlags = IndexEntryFlags(0b100); pub fn is_tracked(self) -> bool { self.0 & 0b001 != 0 } pub fn is_cached(self) -> bool { self.0 & 0b010 != 0 } pub fn is_conflicted(self) -> bool { self.0 & 0b100 != 0 } pub fn with(self, mask: IndexEntryFlags) -> IndexEntryFlags { IndexEntryFlags(self.0 | mask.0) } pub fn without(self, mask: IndexEntryFlags) -> IndexEntryFlags { IndexEntryFlags(self.0 & !mask.0) } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct IndexEntry { pub path: String, pub blob_hash: ObjectId, pub mode: u8, pub flags: IndexEntryFlags, pub mtime_micros: i64, pub size: u64, } #[derive(Clone, Debug, Default)] pub struct Index { pub entries: Vec, } impl Index { pub fn new() -> Self { Self::default() } pub fn find(&self, path: &str) -> Option<&IndexEntry> { self.entries.iter().find(|e| e.path == path) } pub fn find_mut(&mut self, path: &str) -> Option<&mut IndexEntry> { self.entries.iter_mut().find(|e| e.path == path) } pub fn upsert(&mut self, entry: IndexEntry) { if let Some(slot) = self.find_mut(&entry.path) { *slot = entry; } else { self.entries.push(entry); } self.entries.sort_by(|a, b| a.path.cmp(&b.path)); } pub fn remove(&mut self, path: &str) -> bool { if let Some(i) = self.entries.iter().position(|e| e.path == path) { self.entries.remove(i); true } else { false } } pub fn serialize(&self) -> Vec { let mut out = Vec::new(); out.extend_from_slice(&INDEX_MAGIC); let mut v = [0u8; 4]; LittleEndian::write_u32(&mut v, INDEX_VERSION); out.extend_from_slice(&v); let mut c = [0u8; 4]; LittleEndian::write_u32(&mut c, self.entries.len() as u32); out.extend_from_slice(&c); for e in &self.entries { let path_bytes = e.path.as_bytes(); let mut pl = [0u8; 2]; LittleEndian::write_u16(&mut pl, path_bytes.len() as u16); out.extend_from_slice(&pl); out.extend_from_slice(path_bytes); out.extend_from_slice(e.blob_hash.as_bytes()); out.push(e.mode); out.push(e.flags.0); let mut mt = [0u8; 8]; LittleEndian::write_i64(&mut mt, e.mtime_micros); out.extend_from_slice(&mt); let mut sz = [0u8; 8]; LittleEndian::write_u64(&mut sz, e.size); out.extend_from_slice(&sz); } out } pub fn parse(bytes: &[u8]) -> Result { if bytes.len() < 12 { return Err(Error::InvalidIndex("index file too short".into())); } if &bytes[0..4] != INDEX_MAGIC.as_ref() { return Err(Error::InvalidIndex("bad magic".into())); } let version = LittleEndian::read_u32(&bytes[4..8]); if version != INDEX_VERSION { return Err(Error::InvalidIndex(format!( "unsupported version {version}" ))); } let count = LittleEndian::read_u32(&bytes[8..12]) as usize; let mut entries = Vec::with_capacity(count); let mut p = 12usize; for _ in 0..count { if bytes.len() < p + 2 { return Err(Error::InvalidIndex("entry truncated".into())); } let pl = LittleEndian::read_u16(&bytes[p..p + 2]) as usize; p += 2; if bytes.len() < p + pl + 32 + 1 + 1 + 8 + 8 { return Err(Error::InvalidIndex("entry truncated".into())); } let path = std::str::from_utf8(&bytes[p..p + pl]) .map_err(|_| Error::InvalidIndex("path not UTF-8".into()))? .to_string(); p += pl; let mut h = [0u8; 32]; h.copy_from_slice(&bytes[p..p + 32]); p += 32; let mode = bytes[p]; p += 1; let flags = IndexEntryFlags(bytes[p]); p += 1; let mtime_micros = LittleEndian::read_i64(&bytes[p..p + 8]); p += 8; let size = LittleEndian::read_u64(&bytes[p..p + 8]); p += 8; entries.push(IndexEntry { path, blob_hash: ObjectId(h), mode, flags, mtime_micros, size, }); } if p != bytes.len() { return Err(Error::InvalidIndex("trailing bytes after entries".into())); } Ok(Self { entries }) } pub fn read_from(path: &PathBuf) -> Result { match fs::read(path) { Ok(bytes) => Index::parse(&bytes), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Index::new()), Err(e) => Err(Error::Io { path: Some(path.clone()), source: e, }), } } pub fn write_to(&self, path: &PathBuf) -> Result<()> { if let Some(parent) = path.parent() { fs::create_dir_all(parent).ctx(parent.to_path_buf())?; } let tmp = path.with_extension("tmp"); fs::write(&tmp, self.serialize()).ctx(tmp.clone())?; fs::rename(&tmp, path).ctx(path.clone())?; Ok(()) } } #[cfg(test)] mod tests { use super::*; #[test] fn index_roundtrip() { let mut idx = Index::new(); idx.upsert(IndexEntry { path: "src/main.rs".into(), blob_hash: ObjectId([1; 32]), mode: 0, flags: IndexEntryFlags::TRACKED, mtime_micros: 1234, size: 99, }); idx.upsert(IndexEntry { path: "README".into(), blob_hash: ObjectId([2; 32]), mode: 0, flags: IndexEntryFlags::TRACKED, mtime_micros: 4321, size: 1, }); let bytes = idx.serialize(); let idx2 = Index::parse(&bytes).unwrap(); assert_eq!(idx.entries, idx2.entries); } }