file_content/
file.rs

1use std::{
2    fmt::Display,
3    fs,
4    io::{Read, Write},
5    path::{Path, PathBuf},
6};
7
8use crate::{
9    encoding::{to_utf16_be, to_utf16_le, to_utf8_bom, Encoding},
10    text_data::TextData,
11};
12
13/// An enum that represents the possible contents of a file
14/// 
15/// - `Encoded`: The content is a string that can be decoded as one of the
16/// supported encodings from [Encoding] (held in a [TextData])
17/// - `Binary`: The content is a sequence of bytes that cannot be decoded as a string
18#[derive(Debug, PartialEq)]
19pub enum FileContent {
20    Encoded { content: TextData },
21    Binary { content: Vec<u8> },
22}
23
24impl FileContent {
25    pub fn write<T: Write>(&self, writer: &mut T) -> Result<(), std::io::Error> {
26        match self {
27            FileContent::Encoded { content } => match content.encoding {
28                Encoding::Utf8 => writer.write_all(content.data.as_bytes()),
29                Encoding::Utf8Bom => writer.write_all(&to_utf8_bom(&content.data)),
30                Encoding::Utf16Be => writer.write_all(&to_utf16_be(&content.data)),
31                Encoding::Utf16Le => writer.write_all(&to_utf16_le(&content.data)),
32            },
33            FileContent::Binary { content } => writer.write_all(content),
34        }
35    }
36}
37
38/// A file representation that can be used to pair a file path with its content.
39/// [File] provides convenience methods for working with files on disk, or in memory.
40#[derive(Debug, PartialEq)]
41pub struct File {
42    pub path: PathBuf,
43    pub content: FileContent,
44}
45
46/// Represents the possible errors that can occur when working with [File] structs.
47#[derive(Debug, thiserror::Error)]
48pub enum FileError {
49    #[error(transparent)]
50    Io(#[from] std::io::Error),
51
52    #[error(transparent)]
53    TextData(#[from] crate::text_data::TextDataError),
54}
55
56impl Display for File {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        match &self.content {
59            FileContent::Encoded { content } => write!(
60                f,
61                "File: {}\nEncoding: {}\nContent:\n{}",
62                self.path.display(),
63                content.encoding,
64                content.data
65            ),
66            FileContent::Binary { content } => write!(
67                f,
68                "File: {}\nEncoding: Binary\nContent:\n{:?}",
69                self.path.display(),
70                content
71            ),
72        }
73    }
74}
75
76impl File {
77    /// Create a [File] with the given path and read it's content from the input [std::io::Read].
78    /// The encoding is detected as we read the content, and the appropriate [FileContent] is used.
79    pub fn new(path: impl Into<PathBuf>, mut input: impl std::io::Read) -> Result<Self, FileError> {
80        let mut bytes: Vec<u8> = vec![];
81        input.read_to_end(&mut bytes)?;
82        let path = path.into();
83        let content = TextData::try_from(bytes.as_slice());
84        let content = if let Ok(content) = content {
85            FileContent::Encoded { content }
86        } else {
87            FileContent::Binary { content: bytes }
88        };
89
90        Ok(File { path, content })
91    }
92
93    pub fn new_from_path(path: impl Into<PathBuf>) -> Result<Self, FileError> {
94        let path = path.into();
95        let reader = std::fs::File::open(&path)?;
96        Self::new(path, reader)
97    }
98
99    /// Save the content of a file to disk at it's [PathBuf], using the current encoding for the content.
100    pub fn save_to_path(&self) -> Result<(), std::io::Error> {
101        let mut writer = fs::File::create(&self.path)?;
102        self.content.write(&mut writer)
103    }
104}
105
106/// Read the content and return as a [String] if it can be decoded as one of the supported encodings from [Encoding].
107pub fn read_from_reader(mut input: impl Read) -> Result<String, FileError> {
108    let mut bytes = vec![];
109    input.read_to_end(&mut bytes)?;
110    let text_data = TextData::try_from(bytes.as_slice())?;
111    Ok(text_data.data)
112}
113
114/// Read the contents of a file from the given path and return as a [String] if it can be decoded as one of the supported encodings from [Encoding].
115pub fn read_to_string(path: impl AsRef<Path>) -> Result<String, FileError> {
116    Ok(TextData::try_from(path.as_ref())?.data)
117}
118
119#[cfg(test)]
120mod tests {
121    use test_case::test_case;
122
123    use crate::encoding::Encoding;
124    use crate::file::File;
125    use crate::text_data::TextData;
126    use crate::FileContent;
127
128    const UTF8BOM_ASCII_CONTENT: &[u8] = include_bytes!(concat!(
129        env!("CARGO_MANIFEST_DIR"),
130        "/tests/data/UTF8BOM/ascii"
131    ));
132    const UTF16BE_ASCII_CONTENT: &[u8] = include_bytes!(concat!(
133        env!("CARGO_MANIFEST_DIR"),
134        "/tests/data/UTF16BE/ascii"
135    ));
136    const UTF16LE_ASCII_CONTENT: &[u8] = include_bytes!(concat!(
137        env!("CARGO_MANIFEST_DIR"),
138        "/tests/data/UTF16LE/ascii"
139    ));
140
141    #[test_case(b"Hello!", Encoding::Utf8)]
142    #[test_case(UTF8BOM_ASCII_CONTENT, Encoding::Utf8Bom)]
143    #[test_case(UTF16BE_ASCII_CONTENT, Encoding::Utf16Be)]
144    #[test_case(UTF16LE_ASCII_CONTENT, Encoding::Utf16Le)]
145    fn load_from_encoded_content(bytes: &[u8], encoding: Encoding) {
146        let subject = File::new("foo.txt", bytes).expect("Should pass");
147        let expected = File {
148            path: "foo.txt".into(),
149            content: FileContent::Encoded {
150                content: TextData {
151                    data: "Hello!".into(),
152                    encoding,
153                },
154            },
155        };
156
157        assert_eq!(subject, expected);
158    }
159
160    #[test]
161    fn load_from_binary() {
162        let bytes: &[u8] = &[1, 2, 3, 0, 4, 5];
163        let subject = File::new("foo.txt", bytes).expect("Should pass");
164        let expected = File {
165            path: "foo.txt".into(),
166            content: FileContent::Binary {
167                content: (*bytes).to_vec(),
168            },
169        };
170
171        assert_eq!(subject, expected);
172    }
173}