Skip to main content

virtual_file_transfer/
virtual_file.rs

1use crate::Result;
2use crate::file_source::FileSource;
3use crate::source_reader::SourceReader;
4use crate::validation::validate_file_name;
5use std::io;
6
7/// Describes one virtual file exposed to a receiving application.
8///
9/// A virtual file consists of a name, an optional known size, and a
10/// [`crate::FileSource`] that supplies its contents on demand.
11/// The file need not exist as a physical file on the source machine.
12pub struct VirtualFile {
13    name: String,
14    size: Option<u64>,
15    source: Box<dyn FileSource>,
16}
17
18impl VirtualFile {
19    /// Creates a virtual file with an unknown size.
20    ///
21    /// `name` may be a relative path such as `reports/2026/report.txt`.
22    /// The name and its path components are validated for the target platform.
23    /// A separate transfer-level check rejects duplicate or conflicting paths.
24    ///
25    /// # Errors
26    ///
27    /// Returns [`crate::Error::InvalidInput`] when the
28    /// name is empty or invalid.
29    pub fn new(name: &str, source: Box<dyn FileSource>) -> Result<Self> {
30        validate_file_name(name)?;
31
32        Ok(Self {
33            name: name.to_string(),
34            size: None,
35            source,
36        })
37    }
38
39    /// Returns the virtual file's name or relative destination path.
40    pub fn name(&self) -> &str {
41        &self.name
42    }
43
44    /// Returns the explicitly supplied size, if any.
45    ///
46    /// `None` does not imply an empty file. A seekable source may still provide
47    /// its length when opened by the receiving application.
48    pub fn size(&self) -> Option<u64> {
49        self.size
50    }
51
52    /// Sets the advertised size in bytes and returns the updated file.
53    ///
54    /// This does not inspect the source. The caller is responsible for ensuring
55    /// the declared size matches the bytes the source provides.
56    pub fn with_size(mut self, size: u64) -> Self {
57        self.size = Some(size);
58        self
59    }
60
61    pub(crate) fn open(&self) -> io::Result<SourceReader> {
62        self.source.open()
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use crate::{Error, InputError};
70    use std::io::{self, Read};
71    struct StubSource;
72
73    impl FileSource for StubSource {
74        fn open(&self) -> io::Result<SourceReader> {
75            Ok(SourceReader::seekable(std::io::Cursor::new(
76                b"hello".to_vec(),
77            )))
78        }
79    }
80
81    #[test]
82    fn creates_virtual_file() {
83        let file = VirtualFile::new("hello.txt", Box::new(StubSource)).unwrap();
84
85        assert_eq!(file.name(), "hello.txt");
86        assert_eq!(file.size(), None);
87    }
88
89    #[test]
90    fn reads_from_file_source() {
91        let file = VirtualFile::new("hello.txt", Box::new(StubSource)).unwrap();
92
93        let mut reader = file.open().unwrap();
94
95        let mut buffer = [0u8; 5];
96        let count = reader.read(&mut buffer).unwrap();
97
98        assert_eq!(count, 5);
99        assert_eq!(&buffer, b"hello");
100    }
101
102    #[test]
103    fn rejects_empty_name() {
104        let result = VirtualFile::new("", Box::new(StubSource));
105
106        assert!(matches!(
107            result,
108            Err(Error::InvalidInput(InputError::EmptyName))
109        ));
110    }
111}