use core::{fmt, mem};
#[cfg(feature = "std")]
use std::{
fs::File,
io::{self, Read},
path::PathBuf,
};
use crate::{computer::Memory, file::MAX_FILE_SIZE};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Error {
BufferTooLarge(usize),
InvalidNumber(usize, u16),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BufferTooLarge(size) => write!(
f,
"The buffer is too long bytes long ({size} bytes > {MAX_FILE_SIZE} bytes)"
),
Self::InvalidNumber(index, number) => write!(
f,
"A number in the decoded buffer is too large (index {index}, {number} > 999)"
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
#[allow(clippy::module_name_repetitions)]
pub fn load_from_buffer(buffer: &[u8]) -> Result<Memory, Error> {
if buffer.len() > MAX_FILE_SIZE {
return Err(Error::BufferTooLarge(buffer.len()));
}
let mut memory = [0; 100];
let mut address = 0;
let mut offset: u8 = 2;
for (index, byte) in buffer.iter().enumerate() {
let lower = u16::from(*byte) >> (10 - offset);
let upper = (u16::from(*byte) << offset) % 0b100_0000_0000;
memory[address] |= lower;
if index != 0 {
if memory[address] > 999 {
return Err(Error::InvalidNumber(address, memory[address]));
}
address += 1;
}
memory[address] |= upper;
offset += 2;
if offset == 10 {
address -= 1;
offset = 0;
}
}
Ok(unsafe { mem::transmute(memory) })
}
#[cfg(feature = "std")]
#[derive(Debug)]
pub enum FromFileError {
IoError(io::Error),
FileTooLarge(u64),
LoadError(Error),
}
#[cfg(feature = "std")]
impl fmt::Display for FromFileError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::IoError(error) => write!(
f,
"An OS error occurred while reading a file!\nError: {error}"
),
Self::FileTooLarge(size) => write!(
f,
"The file is too large ({size} bytes > {MAX_FILE_SIZE} bytes)"
),
Self::LoadError(error) => fmt::Display::fmt(error, f),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for FromFileError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::IoError(error) => Some(error),
Self::LoadError(error) => Some(error),
Self::FileTooLarge(_) => None,
}
}
}
#[cfg(feature = "std")]
impl From<io::Error> for FromFileError {
fn from(value: io::Error) -> Self {
Self::IoError(value)
}
}
#[cfg(feature = "std")]
impl From<Error> for FromFileError {
fn from(value: Error) -> Self {
Self::LoadError(value)
}
}
#[cfg(feature = "std")]
#[allow(clippy::module_name_repetitions)]
pub fn load_from_file(file: &mut File) -> Result<Memory, FromFileError> {
let file_size = file.metadata()?.len();
if file_size > MAX_FILE_SIZE as u64 {
return Err(FromFileError::FileTooLarge(file_size));
}
let mut buffer = [0; MAX_FILE_SIZE];
let bytes_read = file.read(&mut buffer)?;
load_from_buffer(&buffer[..bytes_read]).map_err(FromFileError::from)
}
#[cfg(feature = "std")]
pub fn load(path: &str) -> Result<Memory, FromFileError> {
load_from_file(&mut File::open(path)?)
}
#[cfg(feature = "std")]
#[allow(clippy::module_name_repetitions)]
pub fn load_from_path(path: PathBuf) -> Result<Memory, FromFileError> {
load_from_file(&mut File::open(path)?)
}
#[cfg(test)]
mod test {
use std::{
env::temp_dir,
fs::{self, File},
io::Write,
};
use uuid::Uuid;
use crate::file::{load, MAX_FILE_SIZE};
use super::load_from_buffer;
#[test]
fn empty_buffer() {
let buffer = [];
let memory = load_from_buffer(&buffer[..]).expect("failed to load from buffer");
assert!(
memory.iter().all(|number| u16::from(*number) == 0),
"Empty buffer did not load all zeros!"
);
}
#[test]
fn full_buffer() {
let mut buffer = [0; MAX_FILE_SIZE];
buffer.iter_mut().enumerate().for_each(|(index, byte)| {
let shift = (2 * index) % 10;
if shift < 8 {
*byte = 0b1000_0000_u8 >> shift;
}
});
let memory = load_from_buffer(&buffer[..]).expect("failed to load from buffer");
assert!(
memory.iter().all(|number| u16::from(*number) == 512),
"Full buffer did not load all 512s!"
);
}
#[test]
fn empty() {
let mut path = temp_dir();
path.push(format!("lminc-test-{}", Uuid::new_v4()));
let file = File::create(path.clone()).expect("failed to create file");
file.sync_all().expect("failed to sync file data");
drop(file);
let path_str = path.to_str().expect("failed to convert path to str");
let memory = load(path_str).expect("failed to load memory from file");
if let Err(error) = fs::remove_file(path.clone()) {
eprintln!("Warning: Failed to remove file ({path_str})!\nError: {error}");
}
assert!(
memory.iter().all(|number| u16::from(*number) == 0),
"Empty file did not load all zeros!"
);
}
#[test]
fn full() {
let mut buffer = [0; MAX_FILE_SIZE];
buffer.iter_mut().enumerate().for_each(|(index, byte)| {
let shift = 8 - 2 * ((index + 1) % 5);
if shift < 8 {
*byte = 1 << shift;
}
});
let mut path = temp_dir();
path.push(format!("lminc-test-{}", Uuid::new_v4()));
let mut file = File::create(path.clone()).expect("failed to create file");
file.write_all(&buffer[..])
.expect("failed to write buffer to file");
file.sync_all().expect("failed to sync file data");
drop(file);
let path_str = path.to_str().expect("failed to convert path to str");
let memory = load(path_str).expect("failed to load memory from file");
if let Err(error) = fs::remove_file(path.clone()) {
eprintln!("Warning: Failed to remove file ({path_str})!\nError: {error}");
}
assert!(
memory.iter().all(|number| u16::from(*number) == 256),
"Full file did not load all 256s!"
);
}
}