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, } #[derive(Debug)] pub enum WriteRepoMetadataError { AlreadyExists, CannotOpenFile(std::io::Error), CannotWriteFile(std::io::Error), } impl From 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 for ReadRepoMetadataError { fn from(value: std::io::Error) -> Self { match value.kind() { ErrorKind::UnexpectedEof => Self::InvalidFormat, _ => Self::CannotReadFile(value), } } } impl From 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 { 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, ReadRepoMetadataError>; fn next(&mut self) -> Option { 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 })) } }