libvctrl 0.5.1

A robust, content-addressed version control engine for arbitrary data, designed for embedding into applications.
Documentation
use crate::codec::{BinaryDecoder, BinaryEncoder, Decoder, Encoder};
use crate::domain::Blob;
use crate::domain::hash::Hash;
use crate::domain::object::Object;
use crate::error::VctrlError;
use crate::storage::traits::{ObjectStore, RefStore};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use std::collections::{HashMap, HashSet};
use std::fs::{File, OpenOptions};
use std::io::{self, BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};

const MAGIC: &[u8; 4] = b"VCTL";
const VERSION: u16 = 1;

const REC_BLOB: u8 = 0x01;
const REC_TREE: u8 = 0x02;
const REC_COMMIT: u8 = 0x03;
const REC_TAG: u8 = 0x04;
const REC_SET_REF: u8 = 0x10;
const REC_DEL_REF: u8 = 0x11;
const REC_SET_HEAD: u8 = 0x12;
const REC_DEL_OBJECT: u8 = 0x20;

struct ObjectInfo {
    rec_type: u8,
    offset: u64,
    length: u32,
}

pub struct FileStore {
    path: PathBuf,
    objects: HashMap<Hash, ObjectInfo>,
    refs: HashMap<String, Hash>,
    head: Option<String>,
    deleted: HashSet<Hash>,
    encoder: BinaryEncoder,
    decoder: BinaryDecoder,
    writer: Option<BufWriter<File>>,
}

impl FileStore {
    pub fn open(path: impl AsRef<Path>) -> Result<Self, VctrlError> {
        let path = path.as_ref().to_path_buf();
        let mut store = Self {
            path: path.clone(),
            objects: HashMap::new(),
            refs: HashMap::new(),
            head: None,
            deleted: HashSet::new(),
            encoder: BinaryEncoder,
            decoder: BinaryDecoder,
            writer: None,
        };
        if path.exists() {
            store.load()?;
        } else {
            let mut file = BufWriter::new(
                OpenOptions::new()
                    .create_new(true)
                    .write(true)
                    .read(true)
                    .open(&path)
                    .map_err(VctrlError::Io)?,
            );
            file.write_all(MAGIC).map_err(VctrlError::Io)?;
            file.write_u16::<BigEndian>(VERSION)
                .map_err(VctrlError::Io)?;
            file.flush().map_err(VctrlError::Io)?;
            store.writer = Some(file);
        }
        Ok(store)
    }

    fn ensure_writer(&mut self) -> Result<&mut BufWriter<File>, VctrlError> {
        if self.writer.is_none() {
            let file = OpenOptions::new()
                .append(true)
                .open(&self.path)
                .map_err(VctrlError::Io)?;
            self.writer = Some(BufWriter::new(file));
        }
        self.writer
            .as_mut()
            .ok_or_else(|| VctrlError::Backend("no writer".into()))
    }

    fn load(&mut self) -> Result<(), VctrlError> {
        let mut file = BufReader::new(File::open(&self.path).map_err(VctrlError::Io)?);
        let mut magic = [0u8; 4];
        if file.read_exact(&mut magic).is_err() {
            return Err(VctrlError::Other("invalid file: too short".into()));
        }
        if &magic != MAGIC {
            return Err(VctrlError::Other("invalid magic".into()));
        }
        let version = file.read_u16::<BigEndian>().map_err(VctrlError::Io)?;
        if version != VERSION {
            return Err(VctrlError::Other(format!(
                "unsupported version {}",
                version
            )));
        }
        let mut deleted_hashes = HashSet::new();
        loop {
            let rec_type = match file.read_u8() {
                Ok(t) => t,
                Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
                Err(e) => return Err(VctrlError::Io(e)),
            };
            match rec_type {
                REC_BLOB | REC_TREE | REC_COMMIT | REC_TAG => {
                    let mut hash_bytes = [0u8; 64];
                    file.read_exact(&mut hash_bytes).map_err(VctrlError::Io)?;
                    let hash = Hash::from_bytes(hash_bytes);
                    let length = file.read_u32::<BigEndian>().map_err(VctrlError::Io)?;
                    let offset = file.stream_position().map_err(VctrlError::Io)?;
                    file.seek(SeekFrom::Current(length as i64))
                        .map_err(VctrlError::Io)?;
                    if !deleted_hashes.contains(&hash) {
                        self.objects.insert(
                            hash,
                            ObjectInfo {
                                rec_type,
                                offset,
                                length,
                            },
                        );
                    }
                }
                REC_SET_REF => {
                    let name_len = file.read_u16::<BigEndian>().map_err(VctrlError::Io)?;
                    let mut name = vec![0u8; name_len as usize];
                    file.read_exact(&mut name).map_err(VctrlError::Io)?;
                    let name =
                        String::from_utf8(name).map_err(|e| VctrlError::Other(e.to_string()))?;
                    let mut h = [0u8; 64];
                    file.read_exact(&mut h).map_err(VctrlError::Io)?;
                    self.refs.insert(name, Hash::from_bytes(h));
                }
                REC_DEL_REF => {
                    let name_len = file.read_u16::<BigEndian>().map_err(VctrlError::Io)?;
                    let mut name = vec![0u8; name_len as usize];
                    file.read_exact(&mut name).map_err(VctrlError::Io)?;
                    let name =
                        String::from_utf8(name).map_err(|e| VctrlError::Other(e.to_string()))?;
                    self.refs.remove(&name);
                }
                REC_SET_HEAD => {
                    let target_len = file.read_u16::<BigEndian>().map_err(VctrlError::Io)?;
                    let mut target = vec![0u8; target_len as usize];
                    file.read_exact(&mut target).map_err(VctrlError::Io)?;
                    let target =
                        String::from_utf8(target).map_err(|e| VctrlError::Other(e.to_string()))?;
                    self.head = Some(target);
                }
                REC_DEL_OBJECT => {
                    let mut h = [0u8; 64];
                    file.read_exact(&mut h).map_err(VctrlError::Io)?;
                    deleted_hashes.insert(Hash::from_bytes(h));
                }
                _ => {
                    return Err(VctrlError::Other(format!(
                        "unknown record type {}",
                        rec_type
                    )));
                }
            }
        }
        for hash in &deleted_hashes {
            self.objects.remove(hash);
        }
        self.deleted = deleted_hashes;
        Ok(())
    }

    fn encode_object(&self, obj: &Object) -> Result<(u8, Vec<u8>), VctrlError> {
        match obj {
            Object::Blob(blob) => Ok((REC_BLOB, blob.as_bytes().to_vec())),
            Object::Tree(tree) => {
                let mut buf = Vec::new();
                self.encoder.encode_tree(tree, &mut buf)?;
                Ok((REC_TREE, buf))
            }
            Object::Commit(commit) => {
                let mut buf = Vec::new();
                self.encoder.encode_commit(commit, &mut buf)?;
                Ok((REC_COMMIT, buf))
            }
            Object::Tag(tag) => {
                let mut buf = Vec::new();
                self.encoder.encode_tag(tag, &mut buf)?;
                Ok((REC_TAG, buf))
            }
        }
    }

    fn decode_object(&self, rec_type: u8, data: &[u8]) -> Result<Object, VctrlError> {
        match rec_type {
            REC_BLOB => Ok(Object::Blob(Blob::new(data.to_vec()))),
            REC_TREE => {
                let tree = self.decoder.decode_tree(data)?;
                Ok(Object::Tree(tree))
            }
            REC_COMMIT => {
                let commit = self.decoder.decode_commit(data)?;
                Ok(Object::Commit(Box::new(commit)))
            }
            REC_TAG => {
                let tag = self.decoder.decode_tag(data)?;
                Ok(Object::Tag(Box::new(tag)))
            }
            _ => Err(VctrlError::Other("invalid object record type".into())),
        }
    }
}

impl ObjectStore for FileStore {
    fn put(&mut self, hash: &Hash, obj: &Object) -> Result<(), VctrlError> {
        if self.objects.contains_key(hash) {
            return Ok(());
        }
        self.deleted.remove(hash);
        let (rec_type, data) = self.encode_object(obj)?;
        let writer = self.ensure_writer()?;
        writer.write_u8(rec_type).map_err(VctrlError::Io)?;
        writer.write_all(hash.as_bytes()).map_err(VctrlError::Io)?;
        writer
            .write_u32::<BigEndian>(data.len() as u32)
            .map_err(VctrlError::Io)?;
        let data_offset = writer.stream_position().map_err(VctrlError::Io)?;
        writer.write_all(&data).map_err(VctrlError::Io)?;
        writer.flush().map_err(VctrlError::Io)?;
        writer.get_mut().sync_all().map_err(VctrlError::Io)?;
        self.objects.insert(
            *hash,
            ObjectInfo {
                rec_type,
                offset: data_offset,
                length: data.len() as u32,
            },
        );
        Ok(())
    }

    fn get(&self, hash: &Hash) -> Result<Option<Object>, VctrlError> {
        if self.deleted.contains(hash) {
            return Ok(None);
        }
        match self.objects.get(hash) {
            Some(info) => {
                let mut file = File::open(&self.path).map_err(VctrlError::Io)?;
                file.seek(SeekFrom::Start(info.offset))
                    .map_err(VctrlError::Io)?;
                let mut buf = vec![0u8; info.length as usize];
                file.read_exact(&mut buf).map_err(VctrlError::Io)?;
                let obj = self.decode_object(info.rec_type, &buf)?;
                Ok(Some(obj))
            }
            None => Ok(None),
        }
    }

    fn exists(&self, hash: &Hash) -> Result<bool, VctrlError> {
        Ok(self.objects.contains_key(hash) && !self.deleted.contains(hash))
    }

    fn all_hashes(&self) -> Result<Vec<Hash>, VctrlError> {
        Ok(self
            .objects
            .keys()
            .filter(|h| !self.deleted.contains(h))
            .copied()
            .collect())
    }

    fn remove(&mut self, hash: &Hash) -> Result<(), VctrlError> {
        if self.objects.remove(hash).is_some() {
            self.deleted.insert(*hash);
            let writer = self.ensure_writer()?;
            writer.write_u8(REC_DEL_OBJECT).map_err(VctrlError::Io)?;
            writer.write_all(hash.as_bytes()).map_err(VctrlError::Io)?;
            writer.flush().map_err(VctrlError::Io)?;
            Ok(())
        } else {
            Err(VctrlError::NotFound(format!("object '{}' not found", hash)))
        }
    }
}

impl RefStore for FileStore {
    fn set_ref(&mut self, name: &str, hash: &Hash) -> Result<(), VctrlError> {
        let writer = self.ensure_writer()?;
        writer.write_u8(REC_SET_REF).map_err(VctrlError::Io)?;
        let name_bytes = name.as_bytes();
        writer
            .write_u16::<BigEndian>(name_bytes.len() as u16)
            .map_err(VctrlError::Io)?;
        writer.write_all(name_bytes).map_err(VctrlError::Io)?;
        writer.write_all(hash.as_bytes()).map_err(VctrlError::Io)?;
        writer.flush().map_err(VctrlError::Io)?;
        self.refs.insert(name.to_string(), *hash);
        Ok(())
    }

    fn get_ref(&self, name: &str) -> Result<Option<Hash>, VctrlError> {
        Ok(self.refs.get(name).copied())
    }

    fn delete_ref(&mut self, name: &str) -> Result<(), VctrlError> {
        if self.refs.remove(name).is_some() {
            let writer = self.ensure_writer()?;
            writer.write_u8(REC_DEL_REF).map_err(VctrlError::Io)?;
            let name_bytes = name.as_bytes();
            writer
                .write_u16::<BigEndian>(name_bytes.len() as u16)
                .map_err(VctrlError::Io)?;
            writer.write_all(name_bytes).map_err(VctrlError::Io)?;
            writer.flush().map_err(VctrlError::Io)?;
        }
        Ok(())
    }

    fn set_head(&mut self, target: &str) -> Result<(), VctrlError> {
        let writer = self.ensure_writer()?;
        writer.write_u8(REC_SET_HEAD).map_err(VctrlError::Io)?;
        let target_bytes = target.as_bytes();
        writer
            .write_u16::<BigEndian>(target_bytes.len() as u16)
            .map_err(VctrlError::Io)?;
        writer.write_all(target_bytes).map_err(VctrlError::Io)?;
        writer.flush().map_err(VctrlError::Io)?;
        self.head = Some(target.to_string());
        Ok(())
    }

    fn head(&self) -> Result<Option<Hash>, VctrlError> {
        match &self.head {
            Some(target) if target.starts_with("refs/") => self.get_ref(target),
            Some(direct) => Hash::from_hex(direct).map(Some).map_err(VctrlError::Hash),
            None => Ok(None),
        }
    }

    fn head_ref_name(&self) -> Result<Option<String>, VctrlError> {
        match &self.head {
            Some(target) if target.starts_with("refs/heads/") => {
                if self.refs.contains_key(target) {
                    Ok(Some(target.clone()))
                } else {
                    Ok(None)
                }
            }
            _ => Ok(None),
        }
    }

    fn list_refs(&self, prefix: &str) -> Result<Vec<String>, VctrlError> {
        Ok(self
            .refs
            .keys()
            .filter(|k| k.starts_with(prefix))
            .cloned()
            .collect())
    }
}