cfdp_simplified/filestore/
mod.rs

1use 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
13// file path normalization taken from cargo
14// https://github.com/rust-lang/cargo/blob/6d6dd9d9be9c91390da620adf43581619c2fa90e/crates/cargo-util/src/paths.rs#L81
15// This has been modified as follows:
16//   -  Does accept root dir `/` as the first entry.
17//   - Operators on Utf8Paths from the camino crate
18fn 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    // if the path begins with any number of rootdir components skip them
27    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
64/// Defines any necessary actions a CFDP File Store implementation
65/// must perform. Assumes any FileStore has a root path it operates relative to.
66pub trait FileStore {
67    /// Returns the path to the target with the root path prepended.
68    /// Used when manipulating the filesystem relative to the root path.
69    fn get_native_path<P: AsRef<Utf8Path>>(&self, path: P) -> Utf8PathBuf;
70
71    /// Creates the directory Relative to the root path.
72    fn create_directory<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()>;
73
74    /// Remove a directory Relative to the root path.
75    fn remove_directory<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()>;
76
77    /// Creates a file relative to the root path.
78    fn create_file<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()>;
79
80    /// Delete a file relative to the root path.
81    fn delete_file<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<()>;
82
83    /// Opens a file relative to the root path with the given options.
84    fn open<P: AsRef<Utf8Path>>(&self, path: P, options: &mut OpenOptions)
85        -> FileStoreResult<File>;
86
87    /// Opens a system temporary file
88    fn open_tempfile(&self) -> FileStoreResult<File>;
89
90    /// Retuns the size of the file on disk relative to the root path.
91    fn get_size<P: AsRef<Utf8Path>>(&self, path: P) -> FileStoreResult<u64>;
92}
93
94/// Store the root path information for a FileStore implementation
95/// using built in rust [std::fs] interface.
96pub 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    /// This is a wrapper around [fs::create_dir]
119    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    /// This function wraps [fs::remove_dir_all]
126    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    /// This is a wrapper around [File::create]
133    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    /// This is a wrapper around [fs::remove_file]
140    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    /// This is an alias for [tempfile::tempfile]
156    fn open_tempfile(&self) -> FileStoreResult<File> {
157        Ok(tempfile()?)
158    }
159
160    /// This function uses [fs::metadata] to read the size of the input file relative to the root path.
161    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)]
169/// CCSDS enumerated checksum types
170pub enum ChecksumType {
171    /// Turn every 4 bytes into a u32 and accumulate
172    Modular = 0,
173    /// This checksum is always 0
174    Null = 15,
175}
176
177/// Computes all pre-defined CCSDS checksums
178pub trait FileChecksum {
179    /// Given the input [ChecksumType] compute the appropriate algorithm.
180    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                // reset the file pointer to the beginning
190                reader.rewind()?;
191
192                let mut checksum: u32 = 0;
193                'outer: loop {
194                    // fill_buffer will return an empty slice when EoF is reached
195                    // on the internal Read instance
196                    let buffer = reader.fill_buf()?;
197
198                    if buffer.is_empty() {
199                        // if nothing was read break from the loop
200
201                        break 'outer;
202                    }
203
204                    // chunks_exact can some times be more efficient than chunks
205                    // we'll have to deal with the remainder anyway.
206                    // Take 4 bytes at a time from the buffer, convert to u32 and add
207                    let mut iter = buffer.chunks_exact(4);
208                    (&mut iter).for_each(|chunk| {
209                        // we can unwrap because we are guaranteed to have a length 4 slice
210                        checksum =
211                            checksum.wrapping_add(u32::from_be_bytes(chunk.try_into().unwrap()));
212                    });
213                    // handle any remainder by resizing to 4-bytes then adding
214                    if !iter.remainder().is_empty() {
215                        let mut remainder = iter.remainder().to_vec();
216                        remainder.resize(4, 0_u8);
217                        // we can unwrap because we are guaranteed to have a length 4 vector
218                        checksum = checksum
219                            .wrapping_add(u32::from_be_bytes(remainder.try_into().unwrap()));
220                    }
221
222                    let len = buffer.len();
223                    // update the internal buffer to let it know
224                    // len bytes were consumed
225                    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}