1use std::collections::HashMap;
4use std::{cell::RefCell, rc::Rc};
5use texlang::vm::TexlangState;
6
7pub trait HasFileSystem {
17 fn file_system(&self) -> Rc<RefCell<dyn FileSystem>> {
18 Rc::new(RefCell::new(RealFileSystem {}))
19 }
20}
21
22pub trait FileSystem {
27 fn read_to_string(&self, path: &std::path::Path) -> std::io::Result<String>;
31
32 fn read_to_bytes(&self, path: &std::path::Path) -> std::io::Result<Vec<u8>>;
36
37 fn write_bytes(&self, path: &std::path::Path, contents: &[u8]) -> std::io::Result<()>;
41}
42
43pub fn read_file_to_string<S: HasFileSystem + TexlangState>(
44 vm: &texlang::vm::VM<S>,
45 file_location: texlang::parse::FileLocation,
46 default_extension: &str,
47) -> Result<(std::path::PathBuf, String), FileReadError> {
48 let file_path = file_location.determine_full_path(
49 vm.working_directory
50 .as_ref()
51 .map(std::path::PathBuf::as_ref),
52 default_extension,
53 );
54 match vm
55 .state
56 .file_system()
57 .borrow_mut()
58 .read_to_string(&file_path)
59 {
60 Ok(source_code) => Ok((file_path, source_code)),
61 Err(err) => Err(FileReadError {
62 title: format!("could not read from `{}`", file_path.display()),
63 underlying_error: err,
64 }),
65 }
66}
67
68pub fn read_file_to_bytes<S: HasFileSystem + TexlangState>(
69 vm: &texlang::vm::VM<S>,
70 file_location: texlang::parse::FileLocation,
71 default_extension: &str,
72) -> Result<(std::path::PathBuf, Vec<u8>), FileReadError> {
73 let file_path = file_location.determine_full_path(
74 vm.working_directory
75 .as_ref()
76 .map(std::path::PathBuf::as_ref),
77 default_extension,
78 );
79 match vm
80 .state
81 .file_system()
82 .borrow_mut()
83 .read_to_bytes(&file_path)
84 {
85 Ok(source_code) => Ok((file_path, source_code)),
86 Err(err) => Err(FileReadError {
87 title: format!("could not read from `{}`", file_path.display()),
88 underlying_error: err,
89 }),
90 }
91}
92
93#[derive(Debug)]
95pub struct FileReadError {
96 pub title: String,
97 pub underlying_error: std::io::Error,
98}
99
100impl texlang::error::TexError for FileReadError {
101 fn kind(&self) -> texlang::error::Kind {
102 texlang::error::Kind::FailedPrecondition
103 }
104
105 fn title(&self) -> String {
106 self.title.clone()
107 }
108
109 fn notes(&self) -> Vec<texlang::error::display::Note> {
110 vec![format!("underlying filesystem error: {}", self.underlying_error).into()]
111 }
112}
113
114pub struct RealFileSystem;
116
117impl FileSystem for RealFileSystem {
118 fn read_to_string(&self, path: &std::path::Path) -> std::io::Result<String> {
119 std::fs::read_to_string(path)
120 }
121 fn read_to_bytes(&self, path: &std::path::Path) -> std::io::Result<Vec<u8>> {
122 std::fs::read(path)
123 }
124 fn write_bytes(&self, path: &std::path::Path, contents: &[u8]) -> std::io::Result<()> {
125 std::fs::write(path, contents)
126 }
127}
128
129#[derive(Default)]
154pub struct InMemoryFileSystem {
155 working_directory: std::path::PathBuf,
156 string_files: HashMap<std::path::PathBuf, String>,
157 bytes_files: HashMap<std::path::PathBuf, Vec<u8>>,
158}
159
160impl InMemoryFileSystem {
161 pub fn new(working_directory: &std::path::Path) -> Self {
165 Self {
166 working_directory: working_directory.into(),
167 string_files: Default::default(),
168 bytes_files: Default::default(),
169 }
170 }
171 pub fn add_string_file(&mut self, relative_path: &str, content: &str) {
175 let mut path = self.working_directory.clone();
176 path.push(relative_path);
177 self.string_files.insert(path, content.to_string());
178 }
179 pub fn add_bytes_file(&mut self, relative_path: &str, content: &[u8]) {
183 let mut path = self.working_directory.clone();
184 path.push(relative_path);
185 self.bytes_files.insert(path, content.into());
186 }
187}
188
189impl FileSystem for InMemoryFileSystem {
190 fn read_to_string(&self, path: &std::path::Path) -> std::io::Result<String> {
191 match self.string_files.get(path) {
192 None => Err(std::io::Error::new(
193 std::io::ErrorKind::NotFound,
194 "not found",
195 )),
196 Some(content) => Ok(content.clone()),
197 }
198 }
199 fn read_to_bytes(&self, path: &std::path::Path) -> std::io::Result<Vec<u8>> {
200 match self.bytes_files.get(path) {
201 None => Err(std::io::Error::new(
202 std::io::ErrorKind::NotFound,
203 "not found",
204 )),
205 Some(content) => Ok(content.clone()),
206 }
207 }
208 fn write_bytes(&self, _: &std::path::Path, _: &[u8]) -> std::io::Result<()> {
209 unimplemented!()
210 }
211}
212
213pub trait HasLogging {
215 fn terminal_out(&self) -> Rc<RefCell<dyn std::io::Write>> {
219 Rc::new(RefCell::new(std::io::stdout()))
220 }
221
222 fn log_file(&self) -> Rc<RefCell<dyn std::io::Write>> {
226 Rc::new(RefCell::new(std::io::sink()))
227 }
228}
229
230pub trait HasTerminalIn {
235 fn terminal_in(&self) -> Rc<RefCell<dyn TerminalIn>> {
236 Rc::new(RefCell::new(std::io::stdin()))
237 }
238}
239
240pub trait TerminalIn {
242 fn read_line(&mut self, prompt: Option<&str>, buffer: &mut String) -> std::io::Result<()>;
244}
245
246impl TerminalIn for std::io::Stdin {
247 fn read_line(&mut self, prompt: Option<&str>, buffer: &mut String) -> std::io::Result<()> {
248 if let Some(prompt) = prompt {
249 eprint!("\n{prompt}")
250 }
251 std::io::Stdin::read_line(self, buffer)?;
252 Ok(())
253 }
254}
255
256#[derive(Default)]
265pub struct MockTerminalIn(usize, Vec<String>);
266
267impl MockTerminalIn {
268 pub fn add_line<S: Into<String>>(&mut self, line: S) {
270 self.1.push(line.into());
271 }
272}
273
274impl TerminalIn for MockTerminalIn {
275 fn read_line(&mut self, _: Option<&str>, buffer: &mut String) -> std::io::Result<()> {
276 match self.1.get(self.0) {
277 None => Err(std::io::Error::new(
278 std::io::ErrorKind::UnexpectedEof,
279 "mock terminal input exhausted",
280 )),
281 Some(line) => {
282 buffer.push_str(line);
283 self.0 += 1;
284 Ok(())
285 }
286 }
287 }
288}