use std::{
fs::{self, File, OpenOptions},
io::{BufRead, BufReader, Error as IOError, Read, Seek},
str::Utf8Error,
time::SystemTimeError,
};
use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
use num_derive::FromPrimitive;
use tempfile::tempfile;
use thiserror::Error;
fn normalize_path(path: &Utf8Path) -> Utf8PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Utf8Component::Prefix(..)) = components.peek().cloned() {
components.next();
Utf8PathBuf::from(c.as_str())
} else {
Utf8PathBuf::new()
};
while let Some(_c @ Utf8Component::RootDir) = components.peek().cloned() {
components.next();
}
for component in components {
match component {
Utf8Component::Prefix(..) => unreachable!(),
Utf8Component::RootDir => {
unreachable!()
}
Utf8Component::CurDir => {}
Utf8Component::ParentDir => {
ret.pop();
}
Utf8Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
pub type FileStoreResult<T> = Result<T, FileStoreError>;
#[derive(Error, Debug)]
pub enum FileStoreError {
#[error("File data storage error: {0}")]
IO(#[from] IOError),
#[error("Error Formating String: {0}")]
Format(#[from] std::fmt::Error),
#[error("Error getting SystemTime: {0}")]
SystemTime(#[from] SystemTimeError),
#[error("Cannot find relative path between {0:} and {1:}.")]
PathDiff(String, String),
#[error("Error converting string from UTF-8: {0:}")]
UTF8(#[from] Utf8Error),
}
pub trait FileStore {
fn get_native_path<P: AsRef<Utf8Path>>(&self, path: P) -> Utf8PathBuf;
fn create_directory<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()>;
fn remove_directory<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()>;
fn create_file<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()>;
fn delete_file<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()>;
fn open<P: AsRef<Utf8Path>>(&self, path: P, options: &mut OpenOptions)
-> FileStoreResult<File>;
fn open_tempfile(&self) -> FileStoreResult<File>;
fn get_size<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<u64>;
}
pub struct NativeFileStore {
root_path: Utf8PathBuf,
}
impl NativeFileStore {
pub fn new<P: AsRef<Utf8Path>>(root_path: P) -> Self {
Self {
root_path: root_path.as_ref().to_owned(),
}
}
}
impl FileStore for NativeFileStore {
fn get_native_path<P: AsRef<Utf8Path>>(&self, path: P) -> Utf8PathBuf {
let path = path.as_ref();
match path.starts_with(&self.root_path) {
true => path.to_path_buf(),
false => {
let normal_path = normalize_path(path);
self.root_path.join(normal_path)
}
}
}
fn create_directory<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()> {
let full_path = self.get_native_path(path);
fs::create_dir(full_path)?;
Ok(())
}
fn remove_directory<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()> {
let full_path = self.get_native_path(path);
fs::remove_dir_all(full_path)?;
Ok(())
}
fn create_file<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()> {
let path = self.get_native_path(path);
let f = File::create(path)?;
f.sync_all().map_err(FileStoreError::IO)
}
fn delete_file<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()> {
let full_path = self.get_native_path(path);
fs::remove_file(full_path)?;
Ok(())
}
fn open<P: AsRef<Utf8Path>>(
&self,
path: P,
options: &mut OpenOptions,
) -> FileStoreResult<File> {
let full_path = self.get_native_path(path);
Ok(options.open(full_path)?)
}
fn open_tempfile(&self) -> FileStoreResult<File> {
Ok(tempfile()?)
}
fn get_size<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<u64> {
let full_path = self.get_native_path(path);
Ok(fs::metadata(full_path)?.len())
}
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, FromPrimitive, PartialEq, Eq)]
pub enum ChecksumType {
Modular = 0,
Null = 15,
}
pub trait FileChecksum {
fn checksum(&mut self, checksum_type: ChecksumType) -> FileStoreResult<u32>;
}
impl<R: Read + Seek + ?Sized> FileChecksum for R {
fn checksum(&mut self, checksum_type: ChecksumType) -> FileStoreResult<u32> {
match checksum_type {
ChecksumType::Null => Ok(0_u32),
ChecksumType::Modular => {
let mut reader = BufReader::new(self);
reader.rewind()?;
let mut checksum: u32 = 0;
'outer: loop {
let buffer = reader.fill_buf()?;
if buffer.is_empty() {
break 'outer;
}
let mut iter = buffer.chunks_exact(4);
(&mut iter).for_each(|chunk| {
checksum =
checksum.wrapping_add(u32::from_be_bytes(chunk.try_into().unwrap()));
});
if !iter.remainder().is_empty() {
let mut remainder = iter.remainder().to_vec();
remainder.resize(4, 0_u8);
checksum = checksum
.wrapping_add(u32::from_be_bytes(remainder.try_into().unwrap()));
}
let len = buffer.len();
reader.consume(len);
}
Ok(checksum)
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
use std::io::Write;
use rstest::*;
use tempfile::TempDir;
#[fixture]
#[once]
fn tempdir_fixture() -> TempDir {
TempDir::new().unwrap()
}
#[fixture]
#[once]
fn test_filestore(tempdir_fixture: &TempDir) -> NativeFileStore {
NativeFileStore::new(
Utf8Path::from_path(tempdir_fixture.path()).expect("Unable to make utf8 tempdir"),
)
}
#[rstest]
fn create_file(test_filestore: &NativeFileStore) {
let path = Utf8Path::new("create_file.txt");
test_filestore.create_file(path).unwrap();
let full_path = test_filestore.get_native_path(path);
assert!(full_path.exists())
}
#[rstest]
fn delete_file(test_filestore: &NativeFileStore) {
let path = Utf8Path::new("delete_file.txt");
test_filestore.create_file(path).unwrap();
let full_path = test_filestore.get_native_path(path);
assert!(full_path.exists());
test_filestore.delete_file(path).unwrap();
assert!(!full_path.exists())
}
#[rstest]
fn create_tmpfile(test_filestore: &NativeFileStore) -> FileStoreResult<()> {
let mut file = test_filestore.open_tempfile()?;
{
file.write_all("hello, world!".as_bytes())?;
file.sync_all()?;
}
file.rewind()?;
let mut recovered_text = String::new();
file.read_to_string(&mut recovered_text)?;
assert_eq!("hello, world!".to_owned(), recovered_text);
Ok(())
}
#[rstest]
fn get_filesize(test_filestore: &NativeFileStore) -> FileStoreResult<()> {
let input_text = "Hello, world!";
let expected = input_text.as_bytes().len() as u64;
{
let mut file =
test_filestore.open("test.dat", OpenOptions::new().create(true).write(true))?;
file.write_all(input_text.as_bytes())?;
file.sync_all()?;
}
let size = test_filestore.get_size("test.dat")?;
assert_eq!(expected, size);
Ok(())
}
#[rstest]
fn checksum_cursor(
#[values(ChecksumType::Null, ChecksumType::Modular)] checksum_type: ChecksumType,
) -> FileStoreResult<()> {
let file_data: Vec<u8> = vec![0x8a, 0x1b, 0x37, 0x44, 0x78, 0x91, 0xab, 0x03, 0x46, 0x12];
let expected_checksum = match &checksum_type {
ChecksumType::Null => 0_u32,
ChecksumType::Modular => 0x48BEE247_u32,
};
let recovered_checksum = std::io::Cursor::new(file_data).checksum(checksum_type)?;
assert_eq!(expected_checksum, recovered_checksum);
Ok(())
}
#[rstest]
fn checksum_file(
test_filestore: &NativeFileStore,
#[values(ChecksumType::Null, ChecksumType::Modular)] checksum_type: ChecksumType,
) -> FileStoreResult<()> {
let file_data: Vec<u8> = vec![0x8a, 0x1b, 0x37, 0x44, 0x78, 0x91, 0xab, 0x03, 0x46, 0x12];
{
let mut file = test_filestore.open(
"checksum.txt",
OpenOptions::new().create(true).truncate(true).write(true),
)?;
file.write_all(file_data.as_slice())?;
file.sync_all()?;
}
let expected_checksum = match &checksum_type {
ChecksumType::Null => 0_u32,
ChecksumType::Modular => 0x48BEE247_u32,
};
let recovered_checksum = {
let mut file =
test_filestore.open("checksum.txt", OpenOptions::new().create(false).read(true))?;
file.checksum(checksum_type)?
};
assert_eq!(expected_checksum, recovered_checksum);
Ok(())
}
#[fixture]
#[once]
fn failure_dir() -> TempDir {
TempDir::new().unwrap()
}
}