211 lines
5.2 KiB
Rust
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 }))
|
|
}
|
|
}
|