blindforge/src/repo.rs
2026-07-27 21:35:40 +02:00

211 lines
5.2 KiB
Rust

use crate::{
api_client::{MAX_PATH_SIZE, MAX_URL_SIZE},
config::Config,
};
use std::{
io::{ErrorKind, Read, Write},
path::Path,
};
const REPO_METADATA_FILE_NAME: &str = "meta.bin";
const VERSION: [u8; 1] = [0];
#[derive(Clone, Debug)]
pub struct RepoMetadata {
pub date: u64,
pub title: String,
pub repo_url: String,
pub commit_hash: String,
pub content: Vec<u8>,
}
#[derive(Debug)]
pub enum WriteRepoMetadataError {
AlreadyExists,
CannotOpenFile(std::io::Error),
CannotWriteFile(std::io::Error),
}
impl From<std::io::Error> for WriteRepoMetadataError {
fn from(value: std::io::Error) -> Self {
match value.kind() {
ErrorKind::AlreadyExists => Self::AlreadyExists,
_ => Self::CannotWriteFile(value),
}
}
}
#[derive(Debug)]
pub enum ReadRepoMetadataError {
CannotOpenFile(std::io::Error),
CannotReadFile(std::io::Error),
InvalidFormat,
UnsupportedVersion,
}
impl From<std::io::Error> for ReadRepoMetadataError {
fn from(value: std::io::Error) -> Self {
match value.kind() {
ErrorKind::UnexpectedEof => Self::InvalidFormat,
_ => Self::CannotReadFile(value),
}
}
}
impl From<std::string::FromUtf8Error> for ReadRepoMetadataError {
fn from(value: std::string::FromUtf8Error) -> Self {
Self::InvalidFormat
}
}
impl RepoMetadata {
pub fn write_to_file(
&self,
config: &Config,
repo_dir: &Path,
) -> Result<(), WriteRepoMetadataError> {
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(repo_dir.join("index"))
.map_err(WriteRepoMetadataError::CannotOpenFile)?;
file.write_all(&VERSION)?;
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
file.write_all(&now.to_be_bytes())?;
file.write_all(&(self.title.len() as u32).to_be_bytes())?;
file.write_all(self.title.as_bytes())?;
file.write_all(&(self.repo_url.len() as u32).to_be_bytes())?;
file.write_all(self.repo_url.as_bytes())?;
file.write_all(&(self.commit_hash.len() as u32).to_be_bytes())?;
file.write_all(self.commit_hash.as_bytes())?;
file.write_all(&self.content)?;
Ok(())
}
pub fn read_from_file(config: &Config, repo_dir: &Path) -> Result<Self, ReadRepoMetadataError> {
let mut file = std::fs::OpenOptions::new()
.read(true)
.open(repo_dir.join("index"))
.map_err(ReadRepoMetadataError::CannotOpenFile)?;
let mut version = [0u8; 1];
file.read_exact(&mut version)?;
if version != [0] {
return Err(ReadRepoMetadataError::UnsupportedVersion);
}
let mut date = [0u8; 8];
file.read_exact(&mut date)?;
let date = u64::from_be_bytes(date);
let mut title_len = [0u8; 4];
file.read_exact(&mut title_len)?;
let title_len = u32::from_be_bytes(title_len) as usize;
if title_len > MAX_URL_SIZE {
return Err(ReadRepoMetadataError::InvalidFormat);
}
let mut title = vec![0; title_len];
file.read_exact(&mut title)?;
let title = String::from_utf8(title)?;
let mut repo_url_len = [0u8; 4];
file.read_exact(&mut repo_url_len)?;
let repo_url_len = u32::from_be_bytes(repo_url_len) as usize;
if repo_url_len > MAX_URL_SIZE {
return Err(ReadRepoMetadataError::InvalidFormat);
}
let mut repo_url = vec![0; repo_url_len];
file.read_exact(&mut repo_url)?;
let repo_url = String::from_utf8(repo_url)?;
let mut commit_hash_len = [0u8; 4];
file.read_exact(&mut commit_hash_len)?;
let commit_hash_len = u32::from_be_bytes(commit_hash_len) as usize;
if commit_hash_len > MAX_URL_SIZE {
return Err(ReadRepoMetadataError::InvalidFormat);
}
let mut commit_hash = vec![0; commit_hash_len];
file.read_exact(&mut commit_hash)?;
let commit_hash = String::from_utf8(commit_hash)?;
let mut content = Vec::new();
file.read_to_end(&mut content)?;
Ok(Self {
date,
title,
repo_url,
commit_hash,
content,
})
}
pub fn iter_files<'a>(&'a self) -> RepoMetadataIter<'a> {
RepoMetadataIter {
repo_metadata: self,
offset: 0,
}
}
}
pub struct RepoMetadataIter<'a> {
repo_metadata: &'a RepoMetadata,
offset: usize,
}
pub struct RepoMetadataEntry<'a> {
pub file_path: &'a str,
pub hash: &'a str,
}
impl<'a> Iterator for RepoMetadataIter<'a> {
type Item = Result<RepoMetadataEntry<'a>, ReadRepoMetadataError>;
fn next(&mut self) -> Option<Self::Item> {
if self.offset == self.repo_metadata.content.len() {
return None;
}
if self.offset + 4 > self.repo_metadata.content.len() {
return Some(Err(ReadRepoMetadataError::InvalidFormat));
}
let file_path_len = u32::from_be_bytes(
self.repo_metadata.content[self.offset..self.offset + 4]
.try_into()
.expect("unreachable"),
) as usize;
if file_path_len > MAX_PATH_SIZE {
return Some(Err(ReadRepoMetadataError::InvalidFormat));
}
if self.offset + file_path_len + 48 > self.repo_metadata.content.len() {
return Some(Err(ReadRepoMetadataError::InvalidFormat));
}
let Ok(file_path) = str::from_utf8(
&self.repo_metadata.content[self.offset + 4..self.offset + 4 + file_path_len],
) else {
return Some(Err(ReadRepoMetadataError::InvalidFormat));
};
let Ok(hash) = str::from_utf8(
&self.repo_metadata.content
[self.offset + 4 + file_path_len..self.offset + 48 + file_path_len],
) else {
return Some(Err(ReadRepoMetadataError::InvalidFormat));
};
self.offset += 48 + file_path_len;
Some(Ok(RepoMetadataEntry { file_path, hash }))
}
}