lminc/file/
load.rs

1use core::{fmt, mem};
2#[cfg(feature = "std")]
3use std::{
4    fs::File,
5    io::{self, Read},
6    path::PathBuf,
7};
8
9use crate::{computer::Memory, file::MAX_FILE_SIZE};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12/// Loading Errors
13pub enum Error {
14    /// The buffer is more than [`MAX_FILE_SIZE`] bytes long
15    BufferTooLarge(usize),
16    /// A number in the decoded buffer is too large (> 999)
17    InvalidNumber(usize, u16),
18}
19
20impl fmt::Display for Error {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        match self {
23            Self::BufferTooLarge(size) => write!(
24                f,
25                "The buffer is too long bytes long ({size} bytes > {MAX_FILE_SIZE} bytes)"
26            ),
27            Self::InvalidNumber(index, number) => write!(
28                f,
29                "A number in the decoded buffer is too large (index {index}, {number} > 999)"
30            ),
31        }
32    }
33}
34
35#[cfg(feature = "std")]
36impl std::error::Error for Error {}
37
38#[allow(clippy::module_name_repetitions)]
39/// Load [Memory] from a saved buffer
40///
41/// # Errors
42/// See [Error]
43pub fn load_from_buffer(buffer: &[u8]) -> Result<Memory, Error> {
44    if buffer.len() > MAX_FILE_SIZE {
45        return Err(Error::BufferTooLarge(buffer.len()));
46    }
47
48    // Initialise the memory, address index and offset
49    let mut memory = [0; 100];
50    let mut address = 0;
51    let mut offset: u8 = 2;
52
53    for (index, byte) in buffer.iter().enumerate() {
54        // Get the parts of the byte in the first and second 10 bit number
55        let lower = u16::from(*byte) >> (10 - offset);
56        let upper = (u16::from(*byte) << offset) % 0b100_0000_0000;
57
58        // Add the lower
59        memory[address] |= lower;
60
61        if index != 0 {
62            // If the latest complete number added is too large, error
63            if memory[address] > 999 {
64                return Err(Error::InvalidNumber(address, memory[address]));
65            }
66
67            // Increment the address
68            address += 1;
69        }
70
71        // Add the upper
72        memory[address] |= upper;
73
74        // Increase the offset, resetting it and decreasing the address index if past 8
75        offset += 2;
76        if offset == 10 {
77            address -= 1;
78            offset = 0;
79        }
80    }
81
82    // The numbers have already been checked and are not
83    //  over 999, so it is safe to transmute
84    Ok(unsafe { mem::transmute(memory) })
85}
86
87#[cfg(feature = "std")]
88#[derive(Debug)]
89/// File-specific loading errors
90pub enum FromFileError {
91    /// Encountered an Os error while performing a file system operation
92    IoError(io::Error),
93    /// The file is more than [MAX_FILE_SIZE] bytes long
94    FileTooLarge(u64),
95    /// The contents of the file could not be loaded, see [Error]
96    LoadError(Error),
97}
98
99#[cfg(feature = "std")]
100impl fmt::Display for FromFileError {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        match self {
103            Self::IoError(error) => write!(
104                f,
105                "An OS error occurred while reading a file!\nError: {error}"
106            ),
107            Self::FileTooLarge(size) => write!(
108                f,
109                "The file is too large ({size} bytes > {MAX_FILE_SIZE} bytes)"
110            ),
111            Self::LoadError(error) => fmt::Display::fmt(error, f),
112        }
113    }
114}
115
116#[cfg(feature = "std")]
117impl std::error::Error for FromFileError {
118    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
119        match self {
120            Self::IoError(error) => Some(error),
121            Self::LoadError(error) => Some(error),
122            Self::FileTooLarge(_) => None,
123        }
124    }
125}
126
127#[cfg(feature = "std")]
128impl From<io::Error> for FromFileError {
129    fn from(value: io::Error) -> Self {
130        Self::IoError(value)
131    }
132}
133
134#[cfg(feature = "std")]
135impl From<Error> for FromFileError {
136    fn from(value: Error) -> Self {
137        Self::LoadError(value)
138    }
139}
140
141#[cfg(feature = "std")]
142#[allow(clippy::module_name_repetitions)]
143/// Load [Memory] from the given file
144///
145/// This function will move the cursor inside file,
146/// unless you know what you are doing, do not use `file` after calling this function
147///
148/// # Errors
149/// See [`FromFileError`]
150pub fn load_from_file(file: &mut File) -> Result<Memory, FromFileError> {
151    // Make sure the file is not too large
152    let file_size = file.metadata()?.len();
153    if file_size > MAX_FILE_SIZE as u64 {
154        return Err(FromFileError::FileTooLarge(file_size));
155    }
156
157    // Read the file
158    let mut buffer = [0; MAX_FILE_SIZE];
159    let bytes_read = file.read(&mut buffer)?;
160
161    // Load it
162    load_from_buffer(&buffer[..bytes_read]).map_err(FromFileError::from)
163}
164
165#[cfg(feature = "std")]
166/// Load [Memory] from a file given the path str
167///
168/// # Errors
169/// See [`FromFileError`]
170pub fn load(path: &str) -> Result<Memory, FromFileError> {
171    load_from_file(&mut File::open(path)?)
172}
173
174#[cfg(feature = "std")]
175#[allow(clippy::module_name_repetitions)]
176/// Load [Memory] from a file given the path
177///
178/// # Errors
179/// See [`FromFileError`]
180pub fn load_from_path(path: PathBuf) -> Result<Memory, FromFileError> {
181    load_from_file(&mut File::open(path)?)
182}
183
184#[cfg(test)]
185mod test {
186    use std::{
187        env::temp_dir,
188        fs::{self, File},
189        io::Write,
190    };
191
192    use uuid::Uuid;
193
194    use crate::file::{load, MAX_FILE_SIZE};
195
196    use super::load_from_buffer;
197
198    #[test]
199    fn empty_buffer() {
200        let buffer = [];
201
202        // Load the memory from the buffer
203        let memory = load_from_buffer(&buffer[..]).expect("failed to load from buffer");
204
205        assert!(
206            memory.iter().all(|number| u16::from(*number) == 0),
207            "Empty buffer did not load all zeros!"
208        );
209    }
210
211    #[test]
212    fn full_buffer() {
213        let mut buffer = [0; MAX_FILE_SIZE];
214        // Set the MSB in each 10-bit sequence to 1
215        buffer.iter_mut().enumerate().for_each(|(index, byte)| {
216            let shift = (2 * index) % 10;
217            if shift < 8 {
218                *byte = 0b1000_0000_u8 >> shift;
219            }
220        });
221
222        // Load the memory from the buffer
223        let memory = load_from_buffer(&buffer[..]).expect("failed to load from buffer");
224
225        assert!(
226            memory.iter().all(|number| u16::from(*number) == 512),
227            "Full buffer did not load all 512s!"
228        );
229    }
230
231    #[test]
232    fn empty() {
233        // Get a new path in the temp directory
234        let mut path = temp_dir();
235        path.push(format!("lminc-test-{}", Uuid::new_v4()));
236
237        // Create the empty file
238        let file = File::create(path.clone()).expect("failed to create file");
239        file.sync_all().expect("failed to sync file data");
240        drop(file);
241
242        let path_str = path.to_str().expect("failed to convert path to str");
243
244        // Load the memory from the file
245        let memory = load(path_str).expect("failed to load memory from file");
246
247        // Try to delete the file
248        if let Err(error) = fs::remove_file(path.clone()) {
249            eprintln!("Warning: Failed to remove file ({path_str})!\nError: {error}");
250        }
251
252        assert!(
253            memory.iter().all(|number| u16::from(*number) == 0),
254            "Empty file did not load all zeros!"
255        );
256    }
257
258    #[test]
259    fn full() {
260        let mut buffer = [0; MAX_FILE_SIZE];
261        // Set the MSB in each 10-bit sequence to 1
262        buffer.iter_mut().enumerate().for_each(|(index, byte)| {
263            let shift = 8 - 2 * ((index + 1) % 5);
264            if shift < 8 {
265                *byte = 1 << shift;
266            }
267        });
268
269        // Get a new path in the temp directory
270        let mut path = temp_dir();
271        path.push(format!("lminc-test-{}", Uuid::new_v4()));
272
273        // Create the file and write the buffer to it
274        let mut file = File::create(path.clone()).expect("failed to create file");
275        file.write_all(&buffer[..])
276            .expect("failed to write buffer to file");
277        file.sync_all().expect("failed to sync file data");
278        drop(file);
279
280        let path_str = path.to_str().expect("failed to convert path to str");
281
282        // Load the memory from the file
283        let memory = load(path_str).expect("failed to load memory from file");
284
285        // Try to delete the file
286        if let Err(error) = fs::remove_file(path.clone()) {
287            eprintln!("Warning: Failed to remove file ({path_str})!\nError: {error}");
288        }
289
290        assert!(
291            memory.iter().all(|number| u16::from(*number) == 256),
292            "Full file did not load all 256s!"
293        );
294    }
295}