libvctrl 0.5.1

A robust, content-addressed version control engine for arbitrary data, designed for embedding into applications.
Documentation
use crate::codec::{Decoder, Encoder};
use crate::domain::commit::Commit;
use crate::domain::hash::Hash;
use crate::domain::tag::Tag;
use crate::domain::tree::{EntryKind, Tree, TreeEntry};
use crate::domain::user::UserID;
use crate::error::VctrlError;
use byteorder::{BigEndian, ReadBytesExt};
use chrono::{TimeZone, Utc};
use std::io::{Cursor, Read};

pub struct BinaryEncoder;

const MAX_TREE_ENTRIES: u32 = 100_000;
const MAX_NAME_LEN: u16 = 255;
const MAX_PARENTS: u32 = 255;
const MAX_MSG_LEN: u32 = 1_048_576;
const MAX_HEADERS: u16 = 100;
const MAX_HEADER_KEY_LEN: u16 = 255;
const MAX_HEADER_VAL_LEN: u32 = 8192;
const MAX_SIG_LEN: u32 = 2048;
const MAX_USER_NAME_LEN: u16 = 255;
const MAX_USER_EMAIL_LEN: u16 = 255;
const MAX_TAG_MSG_LEN: u32 = 1_048_576;

impl Encoder for BinaryEncoder {
    fn encode_tree(&self, tree: &Tree, buf: &mut Vec<u8>) -> Result<(), VctrlError> {
        buf.push(1u8);
        let n = u32::try_from(tree.entries().len())
            .map_err(|_| VctrlError::Other("tree has too many entries to encode".into()))?;
        buf.extend_from_slice(&n.to_be_bytes());
        for entry in tree.entries() {
            let name = entry.name.as_bytes();
            let name_len = u16::try_from(name.len()).map_err(|_| {
                VctrlError::Other(format!("entry name '{}' too long to encode", entry.name))
            })?;
            buf.extend_from_slice(&name_len.to_be_bytes());
            buf.extend_from_slice(name);
            match entry.kind {
                EntryKind::Blob => buf.push(0u8),
                EntryKind::Tree => buf.push(1u8),
            }
            buf.extend_from_slice(entry.hash.as_bytes());
        }
        Ok(())
    }

    fn encode_commit(&self, commit: &Commit, buf: &mut Vec<u8>) -> Result<(), VctrlError> {
        buf.push(2u8);
        buf.extend_from_slice(commit.tree.as_bytes());
        let np = u32::try_from(commit.parents.len())
            .map_err(|_| VctrlError::Other("commit has too many parents".into()))?;
        buf.extend_from_slice(&np.to_be_bytes());
        for p in &commit.parents {
            buf.extend_from_slice(p.as_bytes());
        }
        write_user(&commit.author, buf)?;
        write_user(&commit.committer, buf)?;
        let ts = commit.timestamp.timestamp();
        let ts_ns = commit.timestamp.timestamp_subsec_nanos();
        buf.extend_from_slice(&ts.to_be_bytes());
        buf.extend_from_slice(&ts_ns.to_be_bytes());
        let msg = commit.message.as_bytes();
        let msg_len = u32::try_from(msg.len())
            .map_err(|_| VctrlError::Other("commit message too long".into()))?;
        buf.extend_from_slice(&msg_len.to_be_bytes());
        buf.extend_from_slice(msg);

        let hdr_count = u16::try_from(commit.headers.len())
            .map_err(|_| VctrlError::Other("too many headers".into()))?;
        buf.extend_from_slice(&hdr_count.to_be_bytes());
        for (key, value) in &commit.headers {
            let key_bytes = key.as_bytes();
            let key_len = u16::try_from(key_bytes.len())
                .map_err(|_| VctrlError::Other("header key too long".into()))?;
            buf.extend_from_slice(&key_len.to_be_bytes());
            buf.extend_from_slice(key_bytes);
            let val_bytes = value.as_bytes();
            let val_len = u32::try_from(val_bytes.len())
                .map_err(|_| VctrlError::Other("header value too long".into()))?;
            buf.extend_from_slice(&val_len.to_be_bytes());
            buf.extend_from_slice(val_bytes);
        }

        if let Some(sig) = &commit.signature {
            let sig_len = u32::try_from(sig.len())
                .map_err(|_| VctrlError::Other("signature too long".into()))?;
            buf.extend_from_slice(&sig_len.to_be_bytes());
            buf.extend_from_slice(sig);
        } else {
            buf.extend_from_slice(&0u32.to_be_bytes());
        }
        Ok(())
    }

    fn encode_tag(&self, tag: &Tag, buf: &mut Vec<u8>) -> Result<(), VctrlError> {
        buf.push(2u8);
        buf.extend_from_slice(tag.target.as_bytes());
        write_user(&tag.tagger, buf)?;
        let ts = tag.timestamp.timestamp();
        let ts_ns = tag.timestamp.timestamp_subsec_nanos();
        buf.extend_from_slice(&ts.to_be_bytes());
        buf.extend_from_slice(&ts_ns.to_be_bytes());
        let msg = tag.message.as_bytes();
        let msg_len = u32::try_from(msg.len())
            .map_err(|_| VctrlError::Other("tag message too long".into()))?;
        buf.extend_from_slice(&msg_len.to_be_bytes());
        buf.extend_from_slice(msg);
        if let Some(sig) = &tag.signature {
            let sig_len = u32::try_from(sig.len())
                .map_err(|_| VctrlError::Other("signature too long".into()))?;
            buf.extend_from_slice(&sig_len.to_be_bytes());
            buf.extend_from_slice(sig);
        } else {
            buf.extend_from_slice(&0u32.to_be_bytes());
        }
        Ok(())
    }
}

fn write_user(user: &UserID, buf: &mut Vec<u8>) -> Result<(), VctrlError> {
    let name = user.name.as_bytes();
    let name_len = u16::try_from(name.len())
        .map_err(|_| VctrlError::Other(format!("name '{}' too long", user.name)))?;
    buf.extend_from_slice(&name_len.to_be_bytes());
    buf.extend_from_slice(name);

    let email = user.email.as_bytes();
    let email_len = u16::try_from(email.len())
        .map_err(|_| VctrlError::Other(format!("email '{}' too long", user.email)))?;
    buf.extend_from_slice(&email_len.to_be_bytes());
    buf.extend_from_slice(email);
    Ok(())
}

pub struct BinaryDecoder;

impl Decoder for BinaryDecoder {
    fn decode_tree(&self, data: &[u8]) -> Result<Tree, VctrlError> {
        let mut cursor = Cursor::new(data);
        let version = cursor
            .read_u8()
            .map_err(|e| VctrlError::Other(e.to_string()))?;
        if version != 1 {
            return Err(VctrlError::Other("unsupported tree version".into()));
        }
        let n = cursor
            .read_u32::<BigEndian>()
            .map_err(|e| VctrlError::Other(e.to_string()))?;
        if n > MAX_TREE_ENTRIES {
            return Err(VctrlError::Corrupted("tree entry count too large".into()));
        }
        let mut entries = Vec::with_capacity(n as usize);
        for _ in 0..n {
            let name_len = cursor
                .read_u16::<BigEndian>()
                .map_err(|e| VctrlError::Other(e.to_string()))?;
            if name_len > MAX_NAME_LEN {
                return Err(VctrlError::Corrupted("entry name too long".into()));
            }
            let mut name_bytes = vec![0u8; name_len as usize];
            cursor
                .read_exact(&mut name_bytes)
                .map_err(|e| VctrlError::Other(e.to_string()))?;
            let name =
                String::from_utf8(name_bytes).map_err(|e| VctrlError::Other(e.to_string()))?;
            let kind_byte = cursor
                .read_u8()
                .map_err(|e| VctrlError::Other(e.to_string()))?;
            let kind = match kind_byte {
                0 => EntryKind::Blob,
                1 => EntryKind::Tree,
                _ => return Err(VctrlError::Other("invalid entry kind".into())),
            };
            let mut hash_bytes = [0u8; 64];
            cursor
                .read_exact(&mut hash_bytes)
                .map_err(|e| VctrlError::Other(e.to_string()))?;
            let hash = Hash::from_bytes(hash_bytes);
            entries.push(TreeEntry { name, kind, hash });
        }
        Tree::new(entries).map_err(VctrlError::Tree)
    }

    fn decode_commit(&self, data: &[u8]) -> Result<Commit, VctrlError> {
        let mut cursor = Cursor::new(data);
        let version = cursor
            .read_u8()
            .map_err(|e| VctrlError::Other(e.to_string()))?;
        if version != 1 && version != 2 {
            return Err(VctrlError::Other("unsupported commit version".into()));
        }
        let mut tree_hash = [0u8; 64];
        cursor
            .read_exact(&mut tree_hash)
            .map_err(|e| VctrlError::Other(e.to_string()))?;
        let tree = Hash::from_bytes(tree_hash);
        let np = cursor
            .read_u32::<BigEndian>()
            .map_err(|e| VctrlError::Other(e.to_string()))?;
        if np > MAX_PARENTS {
            return Err(VctrlError::Corrupted("too many parents".into()));
        }
        let mut parents = Vec::with_capacity(np as usize);
        for _ in 0..np {
            let mut h = [0u8; 64];
            cursor
                .read_exact(&mut h)
                .map_err(|e| VctrlError::Other(e.to_string()))?;
            parents.push(Hash::from_bytes(h));
        }
        let author = read_user(&mut cursor)?;
        let committer = read_user(&mut cursor)?;
        let ts = cursor
            .read_i64::<BigEndian>()
            .map_err(|e| VctrlError::Other(e.to_string()))?;
        let ts_ns = cursor
            .read_u32::<BigEndian>()
            .map_err(|e| VctrlError::Other(e.to_string()))?;
        let timestamp = Utc
            .timestamp_opt(ts, ts_ns)
            .single()
            .ok_or_else(|| VctrlError::Other("invalid timestamp".into()))?;
        let msg_len = cursor
            .read_u32::<BigEndian>()
            .map_err(|e| VctrlError::Other(e.to_string()))?;
        if msg_len > MAX_MSG_LEN {
            return Err(VctrlError::Corrupted("commit message too long".into()));
        }
        let mut msg_bytes = vec![0u8; msg_len as usize];
        cursor
            .read_exact(&mut msg_bytes)
            .map_err(|e| VctrlError::Other(e.to_string()))?;
        let message = String::from_utf8(msg_bytes).map_err(|e| VctrlError::Other(e.to_string()))?;

        let headers = if version >= 2 {
            let hdr_count = cursor
                .read_u16::<BigEndian>()
                .map_err(|e| VctrlError::Other(e.to_string()))?;
            if hdr_count > MAX_HEADERS {
                return Err(VctrlError::Corrupted("too many headers".into()));
            }
            let mut hdrs = Vec::with_capacity(hdr_count as usize);
            for _ in 0..hdr_count {
                let key_len = cursor
                    .read_u16::<BigEndian>()
                    .map_err(|e| VctrlError::Other(e.to_string()))?;
                if key_len > MAX_HEADER_KEY_LEN {
                    return Err(VctrlError::Corrupted("header key too long".into()));
                }
                let mut key_bytes = vec![0u8; key_len as usize];
                cursor
                    .read_exact(&mut key_bytes)
                    .map_err(|e| VctrlError::Other(e.to_string()))?;
                let key =
                    String::from_utf8(key_bytes).map_err(|e| VctrlError::Other(e.to_string()))?;
                let val_len = cursor
                    .read_u32::<BigEndian>()
                    .map_err(|e| VctrlError::Other(e.to_string()))?;
                if val_len > MAX_HEADER_VAL_LEN {
                    return Err(VctrlError::Corrupted("header value too long".into()));
                }
                let mut val_bytes = vec![0u8; val_len as usize];
                cursor
                    .read_exact(&mut val_bytes)
                    .map_err(|e| VctrlError::Other(e.to_string()))?;
                let value =
                    String::from_utf8(val_bytes).map_err(|e| VctrlError::Other(e.to_string()))?;
                hdrs.push((key, value));
            }
            hdrs
        } else {
            Vec::new()
        };

        let sig_len = cursor
            .read_u32::<BigEndian>()
            .map_err(|e| VctrlError::Other(e.to_string()))?;
        if sig_len > MAX_SIG_LEN {
            return Err(VctrlError::Corrupted("signature too long".into()));
        }
        let signature = if sig_len == 0 {
            None
        } else {
            let mut sig = vec![0u8; sig_len as usize];
            cursor
                .read_exact(&mut sig)
                .map_err(|e| VctrlError::Other(e.to_string()))?;
            Some(sig)
        };

        Ok(Commit {
            tree,
            parents,
            author,
            committer,
            timestamp,
            message,
            signature,
            headers,
        })
    }

    fn decode_tag(&self, data: &[u8]) -> Result<Tag, VctrlError> {
        let mut cursor = Cursor::new(data);
        let version = cursor
            .read_u8()
            .map_err(|e| VctrlError::Other(e.to_string()))?;
        if version != 1 && version != 2 {
            return Err(VctrlError::Other("unsupported tag version".into()));
        }
        let mut target_hash = [0u8; 64];
        cursor
            .read_exact(&mut target_hash)
            .map_err(|e| VctrlError::Other(e.to_string()))?;
        let target = Hash::from_bytes(target_hash);
        let tagger = read_user(&mut cursor)?;
        let ts = cursor
            .read_i64::<BigEndian>()
            .map_err(|e| VctrlError::Other(e.to_string()))?;
        let ts_ns = cursor
            .read_u32::<BigEndian>()
            .map_err(|e| VctrlError::Other(e.to_string()))?;
        let timestamp = Utc
            .timestamp_opt(ts, ts_ns)
            .single()
            .ok_or_else(|| VctrlError::Other("invalid timestamp".into()))?;
        let msg_len = cursor
            .read_u32::<BigEndian>()
            .map_err(|e| VctrlError::Other(e.to_string()))?;
        if msg_len > MAX_TAG_MSG_LEN {
            return Err(VctrlError::Corrupted("tag message too long".into()));
        }
        let mut msg_bytes = vec![0u8; msg_len as usize];
        cursor
            .read_exact(&mut msg_bytes)
            .map_err(|e| VctrlError::Other(e.to_string()))?;
        let message = String::from_utf8(msg_bytes).map_err(|e| VctrlError::Other(e.to_string()))?;

        let signature = if version >= 2 {
            let sig_len = cursor
                .read_u32::<BigEndian>()
                .map_err(|e| VctrlError::Other(e.to_string()))?;
            if sig_len > MAX_SIG_LEN {
                return Err(VctrlError::Corrupted("signature too long".into()));
            }
            if sig_len == 0 {
                None
            } else {
                let mut sig = vec![0u8; sig_len as usize];
                cursor
                    .read_exact(&mut sig)
                    .map_err(|e| VctrlError::Other(e.to_string()))?;
                Some(sig)
            }
        } else {
            None
        };

        Ok(Tag {
            target,
            tagger,
            timestamp,
            message,
            signature,
        })
    }
}

fn read_user(cursor: &mut Cursor<&[u8]>) -> Result<UserID, VctrlError> {
    let name_len = cursor
        .read_u16::<BigEndian>()
        .map_err(|e| VctrlError::Other(e.to_string()))?;
    if name_len > MAX_USER_NAME_LEN {
        return Err(VctrlError::Corrupted("user name too long".into()));
    }
    let mut name_bytes = vec![0u8; name_len as usize];
    cursor
        .read_exact(&mut name_bytes)
        .map_err(|e| VctrlError::Other(e.to_string()))?;
    let name = String::from_utf8(name_bytes).map_err(|e| VctrlError::Other(e.to_string()))?;
    let email_len = cursor
        .read_u16::<BigEndian>()
        .map_err(|e| VctrlError::Other(e.to_string()))?;
    if email_len > MAX_USER_EMAIL_LEN {
        return Err(VctrlError::Corrupted("user email too long".into()));
    }
    let mut email_bytes = vec![0u8; email_len as usize];
    cursor
        .read_exact(&mut email_bytes)
        .map_err(|e| VctrlError::Other(e.to_string()))?;
    let email = String::from_utf8(email_bytes).map_err(|e| VctrlError::Other(e.to_string()))?;
    Ok(UserID { name, email })
}