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#[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#[derive(Debug, PartialEq)]
41pub struct File {
42 pub path: PathBuf,
43 pub content: FileContent,
44}
45
46#[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 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 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
106pub 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
114pub 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}