diff --git a/client/src/main.rs b/client/src/main.rs index 1adfccb..bcaf789 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -7,6 +7,7 @@ use common::{ CompressionLevel, FileEntryData, RawEntryData, SourceFileEntryData, HEADER, }, object_body::Object as OtherObject, + primitives::{FileMetadata, Timestamp, RWX}, read_header_and_body, read_header_from_file, read_header_from_slice, read_object_into_headers_sync, Hash, Header, Mode, ObjectType, BLOB_KEY, INDEX_KEY, TREE_KEY, }; @@ -37,23 +38,6 @@ impl Deref for Hashed { } impl Hashed { - // fn from_hash(cache: &PathBuf, hash: Hash) -> Self { - // let (dir, file) = hash.get_parts(); - - // let file_path = cache.join(dir).join(file); - - // assert!(file_path.exists()); - - // let reader = T::read_file_and_verify_type(&file_path); - - // drop(reader); - - // Self { - // inner: T::from_file(cache, &file_path), - // hash, - // } - // } - fn from_object(value: T) -> Self { Self { hash: value.get_hash(), @@ -67,30 +51,6 @@ trait Object { fn get_hash(&self) -> Hash; fn get_prefix(&self) -> String; fn write_to(&self, path: &Path); - - // fn from_file(cache: &PathBuf, file: &PathBuf) -> Self; - - // fn read_file_and_verify_type(path: &PathBuf) -> BufReader { - // let f = File::open(file_path).unwrap(); - // let mut reader = BufReader::new(f); - - // let mut data = Vec::new(); - // reader.read_until(0, &mut data); - - // if data.last() == Some(&0) { - // data.pop(); - // } - - // let name = String::from_utf8(data).unwrap(); - - // let (typ, size) = name.split_once(' ').unwrap(); - - // let object_type = ObjectType::from_str(typ); - - // assert!(object_type == T::get_object_type()); - - // reader - // } } struct CacheObject<'a> { @@ -202,6 +162,17 @@ impl<'a> CacheObject<'a> { let mode = Mode::from_str(mode).expect("valid mode"); + let mut timestamp: [u8; 8] = [0; 8]; + file.read_exact(&mut timestamp) + .expect("file to contain timestamp"); + + let timestamp = Timestamp::from(timestamp); + + let mut metadata = [0u8; 1]; + file.read_exact(&mut metadata) + .expect("file to contain metadata"); + let metadata = FileMetadata::from_u8(metadata[0]); + let mut hash: [u8; 32] = [0; 32]; file.read_exact(&mut hash).expect("file to contain hash"); @@ -212,7 +183,9 @@ impl<'a> CacheObject<'a> { let cache_object = CacheObject::from_file(self.cache, &object_file); vec.push(match cache_object.object_type { - ObjectType::Blob => TreeObject::Blob(cache_object.to_blob(mode, name)), + ObjectType::Blob => { + TreeObject::Blob(cache_object.to_blob(mode, timestamp, metadata, name)) + } ObjectType::Tree => TreeObject::Tree(cache_object.to_tree(mode, name)), ObjectType::Index => panic!("Invalid ObjectType in tree"), }) @@ -228,7 +201,13 @@ impl<'a> CacheObject<'a> { } } - fn to_blob(&self, mode: Mode, path: &str) -> Hashed { + fn to_blob( + &self, + mode: Mode, + timestamp: Timestamp, + metadata: FileMetadata, + path: &str, + ) -> Hashed { assert!(self.object_type == ObjectType::Blob); let file = File::open(&self.file).unwrap(); @@ -246,6 +225,8 @@ impl<'a> CacheObject<'a> { path: path.to_string(), file: self.file.clone(), size, + timestamp, + metadata, }, } } @@ -346,6 +327,8 @@ impl Object for Index { trait WithPath { fn get_path_component(&self) -> &String; + fn get_metadata(&self) -> &FileMetadata; + fn get_timestamp(&self) -> &Timestamp; fn get_mode(&self) -> &Mode; } @@ -408,6 +391,20 @@ impl Tree { } } +const ZERO_TIMESTAMP: Timestamp = Timestamp::from_nanos(0); +const ZERO_METADATA: FileMetadata = FileMetadata { + user_permissions: RWX { + read: false, + write: false, + execute: false, + }, + other_permissions: RWX { + read: false, + write: false, + execute: false, + }, + hidden_flag: false, +}; impl WithPath for Tree { fn get_path_component(&self) -> &String { &self.path @@ -416,6 +413,15 @@ impl WithPath for Tree { fn get_mode(&self) -> &Mode { &self.mode } + + fn get_metadata(&self) -> &FileMetadata { + // Trees don't have metadata but we need to return something so we can encode the hidden flag + &ZERO_METADATA + } + + fn get_timestamp(&self) -> &Timestamp { + &ZERO_TIMESTAMP + } } impl Object for Tree { @@ -459,6 +465,8 @@ impl Object for Tree { #[derive(Debug)] struct Blob { mode: Mode, + timestamp: Timestamp, + metadata: FileMetadata, path: String, file: PathBuf, size: u64, @@ -474,6 +482,9 @@ impl Blob { path: path.file_name().unwrap().to_string_lossy().to_string(), size: path.metadata().unwrap().len(), file: path.to_path_buf(), + timestamp: Timestamp::from_system_time(path.metadata().unwrap().modified().unwrap()) + .unwrap(), + metadata: FileMetadata::from(path.metadata().unwrap()), } } @@ -484,7 +495,10 @@ impl Blob { fn hash_and_write(src: &Path, cache: Option<&Path>) -> Hashed { assert!(src.is_file()); - let size = src.metadata().unwrap().len(); + let metadata = src.metadata().unwrap(); + let size = metadata.len(); + let timestamp: Timestamp = metadata.modified().unwrap().try_into().unwrap(); + let metadata = FileMetadata::from(metadata); let prefix = format!("{} {}\0", BLOB_KEY, size); let mut hasher = Sha256::new(); @@ -521,6 +535,8 @@ impl Blob { mode: Mode::Normal, path: src.file_name().unwrap().to_string_lossy().to_string(), file: src.to_path_buf(), + timestamp, + metadata, size, }, } @@ -535,6 +551,14 @@ impl WithPath for Blob { fn get_mode(&self) -> &Mode { &self.mode } + + fn get_metadata(&self) -> &FileMetadata { + &self.metadata + } + + fn get_timestamp(&self) -> &Timestamp { + &self.timestamp + } } impl Object for Blob { @@ -607,6 +631,8 @@ fn get_bytes_from_thing(object: &T, hash: &Hash) -> Vec { path.push(b' '); path.extend_from_slice(object.get_path_component().as_bytes()); path.push(0); + path.extend_from_slice(&object.get_timestamp().as_bytes()); + path.push(object.get_metadata().to_u8()); path.extend_from_slice(&hash.hash); path @@ -728,20 +754,30 @@ fn write_tree(tree: &Tree, path: &Path) { let blob_path = path.join(&blob.path); let file = File::create(blob_path).expect("File to be created"); - let mut writer = BufWriter::new(file); + { + let mut writer = BufWriter::new(file.try_clone().unwrap()); - let cache_file = File::open(&blob.file).unwrap(); - let mut reader = BufReader::new(cache_file); + let cache_file = File::open(&blob.file).unwrap(); + let mut reader = BufReader::new(cache_file); - let _ = read_header_from_file(&mut reader); + let _ = read_header_from_file(&mut reader); - let mut data: [u8; 1024] = [0; 1024]; - while let Ok(num) = reader.read(&mut data) { - if num == 0 { - break; + let mut data: [u8; 1024] = [0; 1024]; + while let Ok(num) = reader.read(&mut data) { + if num == 0 { + break; + } + writer.write_all(&data[..num]).unwrap(); } - writer.write_all(&data[..num]).unwrap(); } + + let mut permissions = file.metadata().unwrap().permissions(); + + blob.metadata.modify_permissions(&mut permissions); + + file.set_permissions(permissions).unwrap(); + + file.set_modified(blob.timestamp.into()).unwrap(); } } @@ -1228,7 +1264,6 @@ enum Commands { }, Restore { - #[arg(short, long)] directory: PathBuf, #[arg(short, long)] index: Hash, @@ -1237,7 +1272,6 @@ enum Commands { }, Cat { - #[arg(long)] hash: Hash, }, diff --git a/common/src/archive.rs b/common/src/archive.rs index 124e190..f83fa75 100644 --- a/common/src/archive.rs +++ b/common/src/archive.rs @@ -85,7 +85,7 @@ impl CompressionLevel { &self, algorithm: CompressionAlgorithm, ) -> Result { - // matrix of compression levels for each algorithm. The first dimension is the algorithm, the second dimension is the level (0-3) + // matrix of compression levels for each algorithm. The first dimension is the algorithm, the second dimension is the level const LEVELS: [[i32; 3]; 4] = [ [0, 0, 0], // None [3, 6, 15], // Zstd diff --git a/common/src/hash.rs b/common/src/hash.rs index e67089f..b455653 100644 --- a/common/src/hash.rs +++ b/common/src/hash.rs @@ -26,6 +26,10 @@ impl Hash { &self.hash_string } + pub fn as_path(&self) -> String { + format!("{}/{}", &self.hash_string[..2], &self.hash_string[2..]) + } + pub fn from_string(value: &str) -> Option { if value.len() != 64 { return None; diff --git a/common/src/object_body.rs b/common/src/object_body.rs index 17d6c6e..ca1a13c 100644 --- a/common/src/object_body.rs +++ b/common/src/object_body.rs @@ -2,7 +2,10 @@ use std::{collections::HashMap, io::Write, str::from_utf8}; use chrono::{DateTime, Utc}; -use crate::{Hash, Mode}; +use crate::{ + primitives::{FileMetadata, Timestamp}, + Hash, Mode, +}; pub trait Object { fn from_data(data: &[u8]) -> Self; @@ -107,6 +110,8 @@ impl Object for Index { pub struct TreeEntry { pub mode: Mode, pub path: String, + pub timestamp: Timestamp, + pub metadata: FileMetadata, pub hash: Hash, } @@ -114,6 +119,7 @@ pub struct TreeEntry { pub struct Tree { pub contents: Vec, } + impl Object for Tree { fn from_data(data: &[u8]) -> Self { let mut contents = Vec::new(); @@ -138,11 +144,23 @@ impl Object for Tree { .expect("mode and filename to be seperated by space"); let mode = Mode::from_str(mode).expect("valid mode"); + let timestamp: [u8; 8] = data[position..position + 8] + .try_into() + .expect("Slice with incorrect length"); + + let metadata_byte = data[position + 8]; + let metadata = FileMetadata::from_u8(metadata_byte); + + let position = position + 9; + let hash = Hash::try_from(&remaining[position..position + 64]).expect("Hash to be valid"); + contents.push(TreeEntry { hash, mode, + timestamp: timestamp.into(), + metadata, path: name.to_string(), }); @@ -160,6 +178,8 @@ impl Object for Tree { data.push(b' '); data.write_all(entry.path.as_bytes())?; data.push(0); + data.write_all(&entry.timestamp.as_bytes())?; + data.push(entry.metadata.to_u8()); data.write_all(&entry.hash.hash)?; Ok(()) diff --git a/common/src/primitives.rs b/common/src/primitives.rs index 6b2feaf..626e413 100644 --- a/common/src/primitives.rs +++ b/common/src/primitives.rs @@ -1,18 +1,21 @@ use crate::{BLOB_KEY, INDEX_KEY, TREE_KEY}; -use std::fmt::Display; +use std::{ + fmt::Display, + fs::Permissions, + os::unix::fs::{MetadataExt, PermissionsExt}, + time::{SystemTime, UNIX_EPOCH}, +}; #[allow(clippy::zero_prefixed_literal)] #[derive(Debug)] pub enum Mode { Tree = 040000, Normal = 100644, - Executable = 100755, SymbolicLink = 120000, } const TREE_MODE: &str = "040000"; const NORMAL_MODE: &str = "100644"; -const EXECUTABLE_MODE: &str = "100755"; const SYMBOLIC_LINK_MODE: &str = "120000"; impl Mode { @@ -21,7 +24,6 @@ impl Mode { match value { TREE_MODE => Some(Mode::Tree), NORMAL_MODE => Some(Mode::Normal), - EXECUTABLE_MODE => Some(Mode::Executable), SYMBOLIC_LINK_MODE => Some(Mode::SymbolicLink), _ => None, } @@ -31,7 +33,6 @@ impl Mode { match self { Self::Tree => TREE_MODE, Self::Normal => NORMAL_MODE, - Self::Executable => EXECUTABLE_MODE, Self::SymbolicLink => SYMBOLIC_LINK_MODE, } } @@ -69,3 +70,201 @@ impl ObjectType { } } } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Timestamp(u64); + +impl Timestamp { + pub fn now() -> anyhow::Result { + Self::from_system_time(SystemTime::now()) + } + + pub fn as_nanos(&self) -> u64 { + self.0 + } + + pub fn from_system_time(time: SystemTime) -> anyhow::Result { + let nanos = time.duration_since(UNIX_EPOCH)?.as_nanos(); + + u64::try_from(nanos).map(Self).map_err(|_| { + anyhow::anyhow!("Timestamp overflow: system time is too far in the future") + }) + } + + pub const fn from_nanos(nanos: u64) -> Self { + Self(nanos) + } + + pub fn to_system_time(&self) -> SystemTime { + UNIX_EPOCH + std::time::Duration::from_nanos(self.0) + } + + pub fn as_bytes(&self) -> [u8; 8] { + Into::<[u8; 8]>::into(*self) + } +} + +impl From<[u8; 8]> for Timestamp { + fn from(value: [u8; 8]) -> Self { + Self(u64::from_le_bytes(value)) + } +} + +impl From for [u8; 8] { + fn from(value: Timestamp) -> Self { + value.0.to_le_bytes() + } +} + +impl TryFrom for Timestamp { + type Error = anyhow::Error; + + fn try_from(value: SystemTime) -> Result { + Self::from_system_time(value) + } +} + +impl From for SystemTime { + fn from(value: Timestamp) -> Self { + value.to_system_time() + } +} + +#[derive(Debug)] +pub struct RWX { + pub read: bool, + pub write: bool, + pub execute: bool, +} + +impl RWX { + pub fn from_u8(byte: u8) -> Self { + Self { + read: byte & 0b100 != 0, + write: byte & 0b010 != 0, + execute: byte & 0b001 != 0, + } + } + + pub fn to_u8(&self) -> u8 { + let mut byte = 0u8; + + if self.read { + byte |= 0b100; + } + if self.write { + byte |= 0b010; + } + if self.execute { + byte |= 0b001; + } + + byte + } +} + +/// BitPacked Layout +/// User Permissions (3 bits) | Other Permissions (3 bits) | Hidden Flag (1 bit) +#[derive(Debug)] +pub struct FileMetadata { + pub user_permissions: RWX, + pub other_permissions: RWX, + pub hidden_flag: bool, +} + +impl FileMetadata { + pub fn from_u8(byte: u8) -> Self { + Self { + user_permissions: RWX::from_u8((byte >> 5) & 0b111), + other_permissions: RWX::from_u8((byte >> 2) & 0b111), + hidden_flag: byte & 0b00000010 != 0, + } + } + + pub fn to_u8(&self) -> u8 { + let mut byte = 0u8; + + byte |= (self.user_permissions.to_u8() & 0b111) << 5; + byte |= (self.other_permissions.to_u8() & 0b111) << 2; + + if self.hidden_flag { + byte |= 0b00000010; + } + + byte + } + + pub fn modify_permissions(&self, permissions: &mut Permissions) { + let mut mode = permissions.mode(); + + // Clear the user and other permission bits we manage (keep group bits + special bits intact) + mode &= !0o707; + + mode |= u32::from(self.user_permissions.to_u8() & 0b111) << 6; + mode |= u32::from(self.other_permissions.to_u8() & 0b111); + + permissions.set_mode(mode); + } +} + +impl From for FileMetadata { + #[cfg(unix)] + fn from(metadata: std::fs::Metadata) -> Self { + let mode = metadata.mode(); + Self { + user_permissions: RWX { + read: mode & 0o400 != 0, + write: mode & 0o200 != 0, + execute: mode & 0o100 != 0, + }, + other_permissions: RWX { + read: mode & 0o004 != 0, + write: mode & 0o002 != 0, + execute: mode & 0o001 != 0, + }, + hidden_flag: false, // Unix doesn't have a hidden attribute in metadata + } + } + + #[cfg(windows)] + fn from(metadata: std::fs::Metadata) -> Self { + use std::os::windows::fs::MetadataExt; + + const FILE_ATTRIBUTE_HIDDEN: u32 = 0x00000002; + + let readonly = metadata.permissions().readonly(); + Self { + user_permissions: RWX { + read: true, + write: !readonly, + execute: false, // Windows doesn't have an execute permission in metadata + }, + other_permissions: RWX { + read: true, + write: !readonly, + execute: false, // Windows doesn't have an execute permission in metadata + }, + hidden_flag: (metadata.file_attributes() & FILE_ATTRIBUTE_HIDDEN) != 0, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn timestamp_roundtrip() { + let timestamp = Timestamp::now().expect("Failed to get current timestamp"); + let bytes = timestamp.as_bytes(); + let converted_timestamp = Timestamp::from(bytes); + assert_eq!(timestamp, converted_timestamp); + } + + #[test] + fn timestamp_is_le() { + let timestamp = Timestamp::now().expect("Failed to get current timestamp"); + let bytes = timestamp.as_bytes(); + assert_eq!(bytes, timestamp.0.to_le_bytes()); + } +} diff --git a/common/src/store.rs b/common/src/store.rs index cd8ac5e..7952044 100644 --- a/common/src/store.rs +++ b/common/src/store.rs @@ -106,13 +106,13 @@ impl Store { } pub async fn exists(&self, hash: &Hash) -> Result { - Ok(self.operator.exists(hash.as_str()).await?) + Ok(self.operator.exists(&hash.as_path()).await?) } pub async fn get_object(&self, hash: &Hash) -> Result> { let mut reader = self .operator - .reader(hash.as_str()) + .reader(&hash.as_path()) .await? .into_futures_async_read(..) .await?; @@ -128,7 +128,7 @@ impl Store { { let mut writer = self .operator - .writer(hash.as_str()) + .writer(&hash.as_path()) .await? .into_futures_async_write();