cvmfs 0.4.2

CernVM-FS client implementation in Rust
Documentation
use crate::{
	common::{CvmfsError, CvmfsResult},
	database_object::DatabaseObject,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefType {
	Catalog = 0,
	Certificate = 1,
	History = 2,
	MetaInfo = 3,
}

impl TryFrom<i32> for RefType {
	type Error = CvmfsError;

	fn try_from(value: i32) -> Result<Self, Self::Error> {
		match value {
			0 => Ok(RefType::Catalog),
			1 => Ok(RefType::Certificate),
			2 => Ok(RefType::History),
			3 => Ok(RefType::MetaInfo),
			_ => Err(CvmfsError::ParseError),
		}
	}
}

#[derive(Debug, Clone)]
pub struct RefEntry {
	pub hash: String,
	pub ref_type: RefType,
	pub timestamp: i64,
}

#[derive(Debug)]
pub struct Reflog {
	pub database_object: DatabaseObject,
}

unsafe impl Sync for Reflog {}

const LIST_REFS_QUERY: &str = "SELECT hash, type, timestamp FROM refs ORDER BY timestamp DESC";
const LIST_REFS_BY_TYPE_QUERY: &str =
	"SELECT hash, type, timestamp FROM refs WHERE type = ?1 ORDER BY timestamp DESC";
const COUNT_REFS_QUERY: &str = "SELECT COUNT(*) FROM refs";
const CONTAINS_HASH_QUERY: &str = "SELECT COUNT(*) FROM refs WHERE hash = ?1";

impl Reflog {
	pub fn new(database_file: &str) -> CvmfsResult<Self> {
		let database_object = DatabaseObject::new(database_file)?;
		Ok(Self { database_object })
	}

	pub fn list_refs(&self) -> CvmfsResult<Vec<RefEntry>> {
		let mut statement = self.database_object.create_prepared_statement(LIST_REFS_QUERY)?;
		let rows = statement.query_map([], |row| {
			Ok((row.get::<_, String>(0)?, row.get::<_, i32>(1)?, row.get::<_, i64>(2)?))
		})?;
		let mut entries = Vec::new();
		for row in rows {
			let (hash, ref_type, timestamp) = row.map_err(CvmfsError::from)?;
			entries.push(RefEntry { hash, ref_type: RefType::try_from(ref_type)?, timestamp });
		}
		Ok(entries)
	}

	pub fn list_refs_by_type(&self, ref_type: RefType) -> CvmfsResult<Vec<RefEntry>> {
		let mut statement =
			self.database_object.create_prepared_statement(LIST_REFS_BY_TYPE_QUERY)?;
		let rows = statement.query_map([ref_type as i32], |row| {
			Ok((row.get::<_, String>(0)?, row.get::<_, i32>(1)?, row.get::<_, i64>(2)?))
		})?;
		let mut entries = Vec::new();
		for row in rows {
			let (hash, rt, timestamp) = row.map_err(CvmfsError::from)?;
			entries.push(RefEntry { hash, ref_type: RefType::try_from(rt)?, timestamp });
		}
		Ok(entries)
	}

	pub fn count_refs(&self) -> CvmfsResult<u64> {
		let mut statement = self.database_object.create_prepared_statement(COUNT_REFS_QUERY)?;
		let count: i64 = statement.query_row([], |row| row.get(0))?;
		Ok(count as u64)
	}

	pub fn contains_hash(&self, hash: &str) -> CvmfsResult<bool> {
		let mut statement = self.database_object.create_prepared_statement(CONTAINS_HASH_QUERY)?;
		let count: i64 = statement.query_row([hash], |row| row.get(0))?;
		Ok(count > 0)
	}
}

#[cfg(test)]
mod tests {
	use rusqlite::Connection;
	use std::path::{Path, PathBuf};

	use super::*;

	fn tmp_db_path(name: &str) -> PathBuf {
		std::env::temp_dir().join(format!("cvmfs_reflog_{}_{}.sqlite", name, std::process::id()))
	}

	fn create_reflog_db(path: &Path, entries: &[(&str, i32, i64)]) {
		let conn = Connection::open(path).unwrap();
		conn.execute(
			"CREATE TABLE refs (hash TEXT NOT NULL, type INTEGER NOT NULL, timestamp INTEGER NOT NULL)",
			[],
		)
		.unwrap();
		for (hash, ref_type, ts) in entries {
			conn.execute(
				"INSERT INTO refs (hash, type, timestamp) VALUES (?1, ?2, ?3)",
				rusqlite::params![hash, ref_type, ts],
			)
			.unwrap();
		}
	}

	#[test]
	fn list_refs_returns_entries() {
		let path = tmp_db_path("list");
		create_reflog_db(&path, &[("abc123", 0, 1000), ("def456", 1, 2000), ("ghi789", 2, 3000)]);
		let reflog = Reflog::new(path.to_str().unwrap()).unwrap();
		let refs = reflog.list_refs().unwrap();
		assert_eq!(refs.len(), 3);
		assert_eq!(refs[0].hash, "ghi789");
		assert_eq!(refs[0].ref_type, RefType::History);
		std::fs::remove_file(&path).ok();
	}

	#[test]
	fn list_refs_empty() {
		let path = tmp_db_path("empty");
		create_reflog_db(&path, &[]);
		let reflog = Reflog::new(path.to_str().unwrap()).unwrap();
		let refs = reflog.list_refs().unwrap();
		assert!(refs.is_empty());
		std::fs::remove_file(&path).ok();
	}

	#[test]
	fn list_refs_by_type_filters() {
		let path = tmp_db_path("bytype");
		create_reflog_db(&path, &[("abc", 0, 100), ("def", 1, 200), ("ghi", 0, 300)]);
		let reflog = Reflog::new(path.to_str().unwrap()).unwrap();
		let catalogs = reflog.list_refs_by_type(RefType::Catalog).unwrap();
		assert_eq!(catalogs.len(), 2);
		assert!(catalogs.iter().all(|r| r.ref_type == RefType::Catalog));
		let certs = reflog.list_refs_by_type(RefType::Certificate).unwrap();
		assert_eq!(certs.len(), 1);
		std::fs::remove_file(&path).ok();
	}

	#[test]
	fn count_refs() {
		let path = tmp_db_path("count");
		create_reflog_db(&path, &[("a", 0, 1), ("b", 1, 2), ("c", 2, 3)]);
		let reflog = Reflog::new(path.to_str().unwrap()).unwrap();
		assert_eq!(reflog.count_refs().unwrap(), 3);
		std::fs::remove_file(&path).ok();
	}

	#[test]
	fn contains_hash_found() {
		let path = tmp_db_path("contains_yes");
		create_reflog_db(&path, &[("abc123", 0, 100)]);
		let reflog = Reflog::new(path.to_str().unwrap()).unwrap();
		assert!(reflog.contains_hash("abc123").unwrap());
		std::fs::remove_file(&path).ok();
	}

	#[test]
	fn contains_hash_not_found() {
		let path = tmp_db_path("contains_no");
		create_reflog_db(&path, &[("abc123", 0, 100)]);
		let reflog = Reflog::new(path.to_str().unwrap()).unwrap();
		assert!(!reflog.contains_hash("xyz999").unwrap());
		std::fs::remove_file(&path).ok();
	}

	#[test]
	fn ref_type_try_from_valid() {
		assert_eq!(RefType::try_from(0).unwrap(), RefType::Catalog);
		assert_eq!(RefType::try_from(1).unwrap(), RefType::Certificate);
		assert_eq!(RefType::try_from(2).unwrap(), RefType::History);
		assert_eq!(RefType::try_from(3).unwrap(), RefType::MetaInfo);
	}

	#[test]
	fn ref_type_try_from_invalid() {
		assert!(RefType::try_from(4).is_err());
		assert!(RefType::try_from(-1).is_err());
	}

	#[test]
	fn invalid_db_path_errors() {
		let result = Reflog::new("/nonexistent/path.db");
		assert!(result.is_err());
	}
}