cfdp_simplified/filestore/
mod.rs1use std::{
2 fs::{self, File, OpenOptions},
3 io::{BufRead, BufReader, Error as IOError, Read, Seek},
4 str::Utf8Error,
5 time::SystemTimeError,
6};
7
8use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
9use num_derive::FromPrimitive;
10use tempfile::tempfile;
11use thiserror::Error;
12
13fn normalize_path(path: &Utf8Path) -> Utf8PathBuf {
19 let mut components = path.components().peekable();
20 let mut ret = if let Some(c @ Utf8Component::Prefix(..)) = components.peek().cloned() {
21 components.next();
22 Utf8PathBuf::from(c.as_str())
23 } else {
24 Utf8PathBuf::new()
25 };
26 while let Some(_c @ Utf8Component::RootDir) = components.peek().cloned() {
28 components.next();
29 }
30
31 for component in components {
32 match component {
33 Utf8Component::Prefix(..) => unreachable!(),
34 Utf8Component::RootDir => {
35 unreachable!()
36 }
37 Utf8Component::CurDir => {}
38 Utf8Component::ParentDir => {
39 ret.pop();
40 }
41 Utf8Component::Normal(c) => {
42 ret.push(c);
43 }
44 }
45 }
46 ret
47}
48
49pub type FileStoreResult<T> = Result<T, FileStoreError>;
50#[derive(Error, Debug)]
51pub enum FileStoreError {
52 #[error("File data storage error: {0}")]
53 IO(#[from] IOError),
54 #[error("Error Formating String: {0}")]
55 Format(#[from] std::fmt::Error),
56 #[error("Error getting SystemTime: {0}")]
57 SystemTime(#[from] SystemTimeError),
58 #[error("Cannot find relative path between {0:} and {1:}.")]
59 PathDiff(String, String),
60 #[error("Error converting string from UTF-8: {0:}")]
61 UTF8(#[from] Utf8Error),
62}
63
64pub trait FileStore {
67 fn get_native_path<P: AsRef<Utf8Path>>(&self, path: P) -> Utf8PathBuf;
70
71 fn create_directory<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()>;
73
74 fn remove_directory<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()>;
76
77 fn create_file<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()>;
79
80 fn delete_file<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()>;
82
83 fn open<P: AsRef<Utf8Path>>(&self, path: P, options: &mut OpenOptions)
85 -> FileStoreResult<File>;
86
87 fn open_tempfile(&self) -> FileStoreResult<File>;
89
90 fn get_size<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<u64>;
92}
93
94pub struct NativeFileStore {
97 root_path: Utf8PathBuf,
98}
99impl NativeFileStore {
100 pub fn new<P: AsRef<Utf8Path>>(root_path: P) -> Self {
101 Self {
102 root_path: root_path.as_ref().to_owned(),
103 }
104 }
105}
106impl FileStore for NativeFileStore {
107 fn get_native_path<P: AsRef<Utf8Path>>(&self, path: P) -> Utf8PathBuf {
108 let path = path.as_ref();
109 match path.starts_with(&self.root_path) {
110 true => path.to_path_buf(),
111 false => {
112 let normal_path = normalize_path(path);
113 self.root_path.join(normal_path)
114 }
115 }
116 }
117
118 fn create_directory<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()> {
120 let full_path = self.get_native_path(path);
121 fs::create_dir(full_path)?;
122 Ok(())
123 }
124
125 fn remove_directory<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()> {
127 let full_path = self.get_native_path(path);
128 fs::remove_dir_all(full_path)?;
129 Ok(())
130 }
131
132 fn create_file<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()> {
134 let path = self.get_native_path(path);
135 let f = File::create(path)?;
136 f.sync_all().map_err(FileStoreError::IO)
137 }
138
139 fn delete_file<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()> {
141 let full_path = self.get_native_path(path);
142 fs::remove_file(full_path)?;
143 Ok(())
144 }
145
146 fn open<P: AsRef<Utf8Path>>(
147 &self,
148 path: P,
149 options: &mut OpenOptions,
150 ) -> FileStoreResult<File> {
151 let full_path = self.get_native_path(path);
152 Ok(options.open(full_path)?)
153 }
154
155 fn open_tempfile(&self) -> FileStoreResult<File> {
157 Ok(tempfile()?)
158 }
159
160 fn get_size<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<u64> {
162 let full_path = self.get_native_path(path);
163 Ok(fs::metadata(full_path)?.len())
164 }
165}
166
167#[repr(u8)]
168#[derive(Debug, Clone, Copy, FromPrimitive, PartialEq, Eq)]
169pub enum ChecksumType {
171 Modular = 0,
173 Null = 15,
175}
176
177pub trait FileChecksum {
179 fn checksum(&mut self, checksum_type: ChecksumType) -> FileStoreResult<u32>;
181}
182
183impl<R: Read + Seek + ?Sized> FileChecksum for R {
184 fn checksum(&mut self, checksum_type: ChecksumType) -> FileStoreResult<u32> {
185 match checksum_type {
186 ChecksumType::Null => Ok(0_u32),
187 ChecksumType::Modular => {
188 let mut reader = BufReader::new(self);
189 reader.rewind()?;
191
192 let mut checksum: u32 = 0;
193 'outer: loop {
194 let buffer = reader.fill_buf()?;
197
198 if buffer.is_empty() {
199 break 'outer;
202 }
203
204 let mut iter = buffer.chunks_exact(4);
208 (&mut iter).for_each(|chunk| {
209 checksum =
211 checksum.wrapping_add(u32::from_be_bytes(chunk.try_into().unwrap()));
212 });
213 if !iter.remainder().is_empty() {
215 let mut remainder = iter.remainder().to_vec();
216 remainder.resize(4, 0_u8);
217 checksum = checksum
219 .wrapping_add(u32::from_be_bytes(remainder.try_into().unwrap()));
220 }
221
222 let len = buffer.len();
223 reader.consume(len);
226 }
227 Ok(checksum)
228 }
229 }
230 }
231}
232
233#[cfg(test)]
234mod test {
235 use super::*;
236
237 use std::io::Write;
238
239 use rstest::*;
240 use tempfile::TempDir;
241
242 #[fixture]
243 #[once]
244 fn tempdir_fixture() -> TempDir {
245 TempDir::new().unwrap()
246 }
247
248 #[fixture]
249 #[once]
250 fn test_filestore(tempdir_fixture: &TempDir) -> NativeFileStore {
251 NativeFileStore::new(
252 Utf8Path::from_path(tempdir_fixture.path()).expect("Unable to make utf8 tempdir"),
253 )
254 }
255
256 #[rstest]
257 fn create_file(test_filestore: &NativeFileStore) {
258 let path = Utf8Path::new("create_file.txt");
259
260 test_filestore.create_file(path).unwrap();
261
262 let full_path = test_filestore.get_native_path(path);
263
264 assert!(full_path.exists())
265 }
266
267 #[rstest]
268 fn delete_file(test_filestore: &NativeFileStore) {
269 let path = Utf8Path::new("delete_file.txt");
270
271 test_filestore.create_file(path).unwrap();
272
273 let full_path = test_filestore.get_native_path(path);
274 assert!(full_path.exists());
275
276 test_filestore.delete_file(path).unwrap();
277
278 assert!(!full_path.exists())
279 }
280
281 #[rstest]
282 fn create_tmpfile(test_filestore: &NativeFileStore) -> FileStoreResult<()> {
283 let mut file = test_filestore.open_tempfile()?;
284
285 {
286 file.write_all("hello, world!".as_bytes())?;
287 file.sync_all()?;
288 }
289 file.rewind()?;
290 let mut recovered_text = String::new();
291 file.read_to_string(&mut recovered_text)?;
292
293 assert_eq!("hello, world!".to_owned(), recovered_text);
294 Ok(())
295 }
296
297 #[rstest]
298 fn get_filesize(test_filestore: &NativeFileStore) -> FileStoreResult<()> {
299 let input_text = "Hello, world!";
300 let expected = input_text.as_bytes().len() as u64;
301 {
302 let mut file =
303 test_filestore.open("test.dat", OpenOptions::new().create(true).write(true))?;
304 file.write_all(input_text.as_bytes())?;
305 file.sync_all()?;
306 }
307
308 let size = test_filestore.get_size("test.dat")?;
309
310 assert_eq!(expected, size);
311 Ok(())
312 }
313
314 #[rstest]
315 fn checksum_cursor(
316 #[values(ChecksumType::Null, ChecksumType::Modular)] checksum_type: ChecksumType,
317 ) -> FileStoreResult<()> {
318 let file_data: Vec<u8> = vec![0x8a, 0x1b, 0x37, 0x44, 0x78, 0x91, 0xab, 0x03, 0x46, 0x12];
319
320 let expected_checksum = match &checksum_type {
321 ChecksumType::Null => 0_u32,
322 ChecksumType::Modular => 0x48BEE247_u32,
323 };
324
325 let recovered_checksum = std::io::Cursor::new(file_data).checksum(checksum_type)?;
326
327 assert_eq!(expected_checksum, recovered_checksum);
328 Ok(())
329 }
330
331 #[rstest]
332 fn checksum_file(
333 test_filestore: &NativeFileStore,
334 #[values(ChecksumType::Null, ChecksumType::Modular)] checksum_type: ChecksumType,
335 ) -> FileStoreResult<()> {
336 let file_data: Vec<u8> = vec![0x8a, 0x1b, 0x37, 0x44, 0x78, 0x91, 0xab, 0x03, 0x46, 0x12];
337
338 {
339 let mut file = test_filestore.open(
340 "checksum.txt",
341 OpenOptions::new().create(true).truncate(true).write(true),
342 )?;
343 file.write_all(file_data.as_slice())?;
344 file.sync_all()?;
345 }
346 let expected_checksum = match &checksum_type {
347 ChecksumType::Null => 0_u32,
348 ChecksumType::Modular => 0x48BEE247_u32,
349 };
350
351 let recovered_checksum = {
352 let mut file =
353 test_filestore.open("checksum.txt", OpenOptions::new().create(false).read(true))?;
354 file.checksum(checksum_type)?
355 };
356
357 assert_eq!(expected_checksum, recovered_checksum);
358 Ok(())
359 }
360
361 #[fixture]
362 #[once]
363 fn failure_dir() -> TempDir {
364 TempDir::new().unwrap()
365 }
366}