LeVCS/crates/levcs-core/src/index.rs

234 lines
7.1 KiB
Rust

//! 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<IndexEntry>,
}
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<u8> {
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<Self> {
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<Self> {
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);
}
}