use core::mem;
use core::convert::{TryInto, TryFrom};
use core::borrow::Borrow;
use core::fmt;
use core::iter::Cycle;
use core::slice;
use std::borrow::Cow;
use std::io::{self, Read, Write, Seek};
use std::collections::{HashMap, HashSet};
use crate::ReadExactEx;
use super::tap::{
BlockType, Header, TapChunkWriter, TapChunkReader, TapChunkRead, TapChunkInfo,
DATA_BLOCK_FLAG, array_name
};
pub use spectrusty_peripherals::storage::microdrives::{
MAX_USABLE_SECTORS, HEAD_SIZE,
Sector, MicroCartridge, MicroCartridgeIdSecIter};
pub fn checksum<I: IntoIterator<Item=B>, B: Borrow<u8>>(iter: I) -> u8 {
iter.into_iter().fold(0, |acc, x| {
let (mut acc, carry) = acc.overflowing_add(*x.borrow());
acc = acc.wrapping_add(carry as u8);
if acc == u8::max_value() { 0 } else { acc }
})
}
pub trait MicroCartridgeExt: Sized {
fn file_info<S: AsRef<[u8]>>(&self, file_name: S) -> Result<Option<CatFile>, MdrValidationError>;
fn file_type<S: AsRef<[u8]>>(&self, file_name: S) -> Result<Option<CatFileType>, MdrValidationError>;
fn retrieve_file<S: AsRef<[u8]>, W: Write>(&self, file_name: S, wr: W) -> io::Result<Option<(CatFileType, usize)>>;
fn store_file<S: AsRef<[u8]>, R: Read>(&mut self, file_name: S, is_save: bool, rd: R) -> io::Result<u8>;
fn erase_file<S: AsRef<[u8]>>(&mut self, file_name: S) -> u8;
fn file_to_tap_writer<S: AsRef<[u8]>, W: Write + Seek>(&self, file_name: S, wr: &mut TapChunkWriter<W>) -> io::Result<bool>;
fn file_from_tap_reader<R: Read + Seek>(&mut self, rd: &mut TapChunkReader<R>) -> io::Result<u8>;
fn file_sector_ids_unordered<S: AsRef<[u8]>>(&self, file_name: S) -> FileSectorIdsUnordIter<'_>;
fn file_sectors<S: AsRef<[u8]>>(&self, file_name: S) -> FileSectorIter<'_>;
fn catalog(&self) -> Result<Option<Catalog>, MdrValidationError>;
fn catalog_name(&self) -> Result<Option<Cow<'_, str>>, MdrValidationError>;
fn validate_sectors(&self) -> Result<usize, MdrValidationError>;
fn count_sectors_in_use(&self) -> usize;
fn from_mdr<R: Read>(rd: R, max_sectors: usize) -> io::Result<Self>;
fn write_mdr<W: Write>(&self, wr: W) -> io::Result<usize>;
fn new_formatted<S: AsRef<[u8]>>(max_sectors: usize, catalog_name: S) -> Self;
}
pub trait SectorExt {
fn is_free(&self) -> bool;
fn catalog_name(&self) -> &[u8;10];
fn file_name(&self) -> &[u8;10];
fn file_name_matches(&self, name: &[u8]) -> bool;
fn file_type(&self) -> Result<Option<CatFileType>, &'static str>;
fn sector_seq(&self) -> u8;
fn file_block_seq(&self) -> u8;
fn file_block_len(&self) -> u16;
fn is_last_file_block(&self) -> bool;
fn is_save_file(&self) -> bool;
fn data(&self) -> &[u8];
fn data_mut(&mut self) -> &mut [u8];
fn data_record(&self) -> &[u8];
fn set_file_name(&mut self, file_name: &[u8]);
fn set_file_block_seq(&mut self, block_seq: u8);
fn set_file_block_len(&mut self, len: u16);
fn set_last_file_block(&mut self, eof: bool);
fn set_save_file_flag(&mut self, is_save: bool);
fn update_block_checksums(&mut self);
fn erase(&mut self);
fn format(&mut self, seq: u8, catalog_name: &[u8]);
fn validate(&self) -> Result<(), &'static str>;
fn file_header(&self) -> Result<Option<&MdrFileHeader>, &'static str>;
fn tap_header(&self) -> Result<Option<Header>, &'static str>;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MdrValidationError {
pub index: u8,
pub description: &'static str
}
#[derive(Clone, Debug)]
pub struct Catalog {
pub name: String,
pub files: HashMap<[u8;10], CatFile>,
pub sectors_free: u8
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct CatFile {
pub size: u32,
pub blocks: u8,
pub copies: u8,
pub file_type: CatFileType
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CatFileType {
Data,
File(BlockType)
}
pub struct FileSectorIter<'a> {
counter: u8,
stop: u8,
block_seq: u8,
name: [u8; 10],
iter: Cycle<MicroCartridgeIdSecIter<'a>>
}
pub struct FileSectorIdsUnordIter<'a> {
name: [u8; 10],
iter: MicroCartridgeIdSecIter<'a>
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SectorBlock {
pub index: u8,
pub block_seq: u8
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[repr(C,packed)]
pub struct MdrFileHeader {
pub block_type: BlockType,
pub length: [u8;2],
pub start: [u8;2],
pub prog_length: [u8;2],
pub line: [u8;2]
}
#[repr(C)]
union MdrFileHeaderArray {
header: MdrFileHeader,
array: [u8; mem::size_of::<MdrFileHeader>()],
}
impl MdrFileHeader {
pub fn array_name(&self) -> char {
array_name(self.prog_length[0])
}
pub fn length(&self) -> u16 {
u16::from_le_bytes(self.length)
}
pub fn start(&self) -> u16 {
u16::from_le_bytes(self.start)
}
pub fn prog_length(&self) -> u16 {
u16::from_le_bytes(self.prog_length)
}
pub fn line(&self) -> u16 {
u16::from_le_bytes(self.line)
}
pub fn into_array(self) -> [u8; mem::size_of::<MdrFileHeader>()] {
unsafe { MdrFileHeaderArray { header: self }.array }
}
}
impl From<&'_ Header> for MdrFileHeader {
fn from(header: &Header) -> Self {
let block_type = header.block_type;
let length = header.length.to_le_bytes();
let mut start = header.par1;
let mut prog_length = [0, 0];
let mut line = [0, 0];
match block_type {
BlockType::Program => {
start = 0x5CCBu16.to_le_bytes();
line = header.par1;
prog_length = header.par2;
}
BlockType::NumberArray|BlockType::CharArray => {
start = 0x5CCBu16.to_le_bytes();
prog_length[0] = header.par1[1];
}
BlockType::Code => {}
}
MdrFileHeader { block_type, length, start, prog_length, line }
}
}
impl From<&'_ MdrFileHeader> for Header {
fn from(fbh: &MdrFileHeader) -> Self {
let length = u16::from_le_bytes(fbh.length);
match fbh.block_type {
BlockType::Program => {
let vars = u16::from_le_bytes(fbh.prog_length);
let line = u16::from_le_bytes(fbh.line);
Header::new_program(length).with_start(line).with_vars(vars)
}
BlockType::NumberArray => {
Header::new_number_array(length).with_array_name(fbh.array_name())
}
BlockType::CharArray => {
Header::new_char_array(length).with_array_name(fbh.array_name())
}
BlockType::Code => {
let start = u16::from_le_bytes(fbh.start);
Header::new_code(length).with_start(start)
}
}
}
}
impl TryFrom<&[u8]> for &MdrFileHeader {
type Error = &'static str;
fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
let data: &[u8; mem::size_of::<MdrFileHeader>()] = data.try_into()
.map_err(|_| "wrong size of slice for MdrFileHeader")?;
BlockType::try_from(data[0]).map_err(|_| "file type not recognized" )?;
let ptr = data as *const _ as *const MdrFileHeader;
unsafe { Ok(&*ptr) }
}
}
const HDFLAG: usize = 0; const HDNUMB: usize = 1; const HDNAME: usize = 4; const HDCHK: usize = 14;
const RECFLG: usize = 0; const RECNUM: usize = 1; const RECLEN: usize = 2; const RECNAM: usize = 4; const DESCHK: usize = 14; const HD_00: usize = 15; const HD_SIZE: usize = 9; const DCHK: usize = 527;
const RECFLG_EOF: u8 = 2;
const RECFLG_SAVE: u8 = 4;
const BLOCK_DATA_MAX: u16 = 512;
fn copy_name(target: &mut [u8;10], name: &[u8]) {
let name_len = name.len().min(10);
target[..name_len].copy_from_slice(&name[..name_len]);
target[name_len..].iter_mut().for_each(|p| *p=b' ');
}
impl SectorExt for Sector {
fn is_free(&self) -> bool {
!self.is_last_file_block() && self.file_block_len() < BLOCK_DATA_MAX
}
fn catalog_name(&self) -> &[u8;10] {
self.head[HDNAME..HDNAME + 10].try_into().unwrap()
}
fn file_name(&self) -> &[u8;10] {
self.data[RECNAM..RECNAM + 10].try_into().unwrap()
}
fn file_name_matches(&self, name: &[u8]) -> bool {
let name = &name[0..name.len().min(10)];
if &self.data[RECNAM..RECNAM + name.len()] == name {
return self.data[RECNAM + name.len()..RECNAM + 10].iter().all(|p| *p==b' ')
}
false
}
fn file_type(&self) -> Result<Option<CatFileType>, &'static str> {
if self.is_free() {
return Ok(None)
}
Ok(Some(if self.is_save_file() {
CatFileType::File(BlockType::try_from(self.data[HD_00])
.map_err(|_| "file type not recognized" )?)
}
else {
CatFileType::Data
}))
}
fn sector_seq(&self) -> u8 {
self.head[HDNUMB]
}
fn file_block_seq(&self) -> u8 {
self.data[RECNUM]
}
fn file_block_len(&self) -> u16 {
let reclen = &self.data[RECLEN..RECLEN + 2];
u16::from_le_bytes(<[u8;2]>::try_from(reclen).unwrap())
}
fn is_last_file_block(&self) -> bool {
self.data[RECFLG] & RECFLG_EOF != 0
}
fn is_save_file(&self) -> bool {
self.data[RECFLG] & RECFLG_SAVE != 0
}
fn data(&self) -> &[u8] {
&self.data[DESCHK + 1..DCHK]
}
fn data_mut(&mut self) -> &mut [u8] {
&mut self.data[DESCHK + 1..DCHK]
}
fn data_record(&self) -> &[u8] {
let reclen = self.file_block_len() as usize;
&self.data[DESCHK + 1..DESCHK + 1 + reclen]
}
fn set_file_name(&mut self, file_name: &[u8]) {
let block_name = &mut self.data[RECNAM..RECNAM + 10];
copy_name(block_name.try_into().unwrap(), file_name);
}
fn set_file_block_seq(&mut self, block_seq: u8) {
self.data[RECNUM] = block_seq;
}
fn set_file_block_len(&mut self, len: u16) {
if len > BLOCK_DATA_MAX {
panic!("RECLEN must be less than 512");
}
let reclen = len.to_le_bytes();
self.data[RECLEN..RECLEN + 2].copy_from_slice(&reclen);
}
fn set_last_file_block(&mut self, eof: bool) {
if eof {
self.data[RECFLG] |= RECFLG_EOF;
}
else {
self.data[RECFLG] &= !RECFLG_EOF;
}
}
fn set_save_file_flag(&mut self, is_save: bool) {
if is_save {
self.data[RECFLG] |= RECFLG_SAVE;
}
else {
self.data[RECFLG] &= !RECFLG_SAVE;
}
}
fn update_block_checksums(&mut self) {
self.data[DESCHK] = checksum(&self.data[0..DESCHK]);
self.data[DCHK] = checksum(&self.data[DESCHK + 1..DCHK])
}
fn erase(&mut self) {
self.data[RECFLG] = 0;
self.data[RECLEN..RECLEN + 2].iter_mut().for_each(|p| *p=0);
self.data[DESCHK] = checksum(&self.data[0..DESCHK]);
}
fn format(&mut self, seq: u8, catalog_name: &[u8]) {
if !(1..=245).contains(&seq) {
panic!("HDNUMB must be between 1 and 254");
}
self.head[HDFLAG] = 1;
self.head[HDNUMB] = seq;
let head_name = &mut self.head[HDNAME..HDNAME + 10];
copy_name(head_name.try_into().unwrap(), catalog_name);
self.head[HDCHK] = checksum(&self.head[0..HDCHK]);
self.erase();
}
fn validate(&self) -> Result<(), &'static str> {
if self.head[HDFLAG] & 1 != 1 {
return Err("bad sector header: HDFLAG bit 0 is reset");
}
if self.data[RECFLG] & 1 != 0 {
return Err("bad data block: RECFLG bit 0 is set");
}
if !(1..=254).contains(&self.head[HDNUMB]) {
return Err("bad sector header: HDNUMB is outside the allowed range: [1-254]");
}
if self.file_block_len() > BLOCK_DATA_MAX {
return Err("bad data block: RECLEN is larger than the sector size: 512");
}
if self.head[HDCHK] != checksum(&self.head[0..HDCHK]) {
return Err("bad sector header: HDCHK invalid checksum");
}
if self.data[DESCHK] != checksum(&self.data[0..DESCHK]) {
return Err("bad data block: DESCHK invalid header checksum");
}
if !self.is_free() &&
self.data[DCHK] != checksum(&self.data[DESCHK+1..DCHK])
{
return Err("bad data block: DESCHK invalid data checksum");
}
Ok(())
}
fn file_header(&self) -> Result<Option<&MdrFileHeader>, &'static str> {
if !self.is_free() && self.is_save_file() && self.file_block_seq() == 0 {
let mdrhd = <&MdrFileHeader>::try_from(&self.data[HD_00..HD_00 + HD_SIZE])?;
Ok(Some(mdrhd))
}
else {
Ok(None)
}
}
fn tap_header(&self) -> Result<Option<Header>, &'static str> {
self.file_header().map(|m|
m.map(|hd| Header::from(hd).with_name(&self.data[RECNAM..RECNAM + 10]))
)
}
}
impl Default for CatFileType {
fn default() -> Self {
CatFileType::Data
}
}
impl fmt::Display for Catalog {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "{:10}:", self.name)?;
for (name, file) in self.files.iter() {
writeln!(f, " {:10} {}", String::from_utf8_lossy(name), file)?;
}
writeln!(f, "free: {} sec. {} bytes",
self.sectors_free,
self.sectors_free as u32 * BLOCK_DATA_MAX as u32)
}
}
impl fmt::Display for CatFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {:5} bytes {:2} blk.", self.file_type, self.size, self.blocks)?;
if self.copies != 1 {
write!(f, " {} copies", self.copies)?;
}
Ok(())
}
}
impl fmt::Display for CatFileType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CatFileType::Data => f.write_str("Data"),
CatFileType::File(bt) => bt.fmt(f),
}
}
}
impl std::error::Error for MdrValidationError {}
impl fmt::Display for MdrValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "MDR validation error in sector #{}: {}", self.index, self.description)
}
}
impl<'a> Iterator for FileSectorIter<'a> {
type Item = Result<&'a Sector, MdrValidationError>;
fn next(&mut self) -> Option<Self::Item> {
if self.counter == 0 {
return None
}
for (index, sector) in self.iter.by_ref() {
if self.stop == index {
self.counter = 0;
}
else {
self.counter -= 1;
if !sector.is_free() && sector.file_name() == &self.name
&& sector.file_block_seq() == self.block_seq
{
if sector.is_last_file_block() {
self.counter = 0;
}
else {
self.stop = index;
self.counter = !0;
self.block_seq += 1;
}
return Some(Ok(sector))
}
}
if self.counter == 0 {
if self.block_seq != 0 {
return Some(Err(MdrValidationError { index, description:
"missing end of file block" }))
}
break
}
}
None
}
}
impl<'a> Iterator for FileSectorIdsUnordIter<'a> {
type Item = SectorBlock;
fn next(&mut self) -> Option<Self::Item> {
for (index, sector) in self.iter.by_ref() {
if !sector.is_free() && sector.file_name() == &self.name {
let block_seq = sector.file_block_seq();
return Some(SectorBlock { index, block_seq })
}
}
None
}
}
impl MicroCartridgeExt for MicroCartridge {
fn file_info<S: AsRef<[u8]>>(
&self,
file_name: S
) -> Result<Option<CatFile>, MdrValidationError>
{
let file_name = file_name.as_ref();
let mut meta = CatFile::default();
for (index, sector) in self.iter_with_indices() {
if !sector.is_free() && sector.file_name_matches(file_name) {
if sector.file_block_seq() == 0 {
meta.copies += 1;
meta.file_type = sector.file_type()
.map_err(|e| MdrValidationError { index, description: e })?
.unwrap();
}
meta.blocks += 1;
meta.size += sector.file_block_len() as u32;
}
}
if meta.copies != 0 {
meta.size /= meta.copies as u32;
Ok(Some(meta))
}
else {
Ok(None)
}
}
fn file_type<S: AsRef<[u8]>>(
&self,
file_name: S
) -> Result<Option<CatFileType>, MdrValidationError>
{
let file_name = file_name.as_ref();
for (index, sector) in self.iter_with_indices() {
if !sector.is_free() && sector.file_block_seq() == 0 && sector.file_name_matches(file_name) {
return sector.file_type().map_err(|e| MdrValidationError { index, description: e })
}
}
Ok(None)
}
fn file_to_tap_writer<S: AsRef<[u8]>, W: Write + Seek>(
&self,
file_name: S,
wr: &mut TapChunkWriter<W>
) -> io::Result<bool>
{
let mut sector_iter = self.file_sectors(file_name);
#[allow(clippy::never_loop)]
let mut tran = loop {
if let Some(sector) = sector_iter.next() {
let sector = sector.unwrap();
if let Some(header) = sector.tap_header()
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?
{
wr.write_header(&header)?;
let mut tran = wr.begin()?;
tran.write_all(slice::from_ref(&DATA_BLOCK_FLAG))?;
tran.write_all(§or.data_record()[HD_SIZE..])?;
break tran
}
}
return Ok(false)
};
for sector in sector_iter {
let sector = sector.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let record = sector.data_record();
tran.write_all(record)?;
}
tran.commit(true)?;
Ok(true)
}
fn file_from_tap_reader<R: Read + Seek>(&mut self, rd: &mut TapChunkReader<R>) -> io::Result<u8> {
if rd.chunk_limit() == 0 {
rd.next_chunk()?;
}
let header = if let TapChunkInfo::Head(header) = TapChunkInfo::try_from(rd.get_mut())? {
header
}
else {
return Err(io::Error::new(io::ErrorKind::InvalidData, "not a TAP header"))
};
if let Some(chunk_size) = rd.next_chunk()? {
if chunk_size < 2 || header.length != chunk_size - 2 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "not a TAP block"))
}
let mut flag = 0u8;
rd.read_exact(slice::from_mut(&mut flag))?;
if flag != DATA_BLOCK_FLAG {
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid TAP block flag"))
}
}
else {
return Err(io::Error::new(io::ErrorKind::InvalidData, "missing TAP chunk"))
}
let fbheader = MdrFileHeader::from(&header).into_array();
let hdrd = io::Cursor::new(fbheader).chain(rd.take(header.length as u64));
let res = self.store_file(header.name, true, hdrd)?;
{
let mut checksum = 0u8;
let res = rd.read_exact(slice::from_mut(&mut checksum));
if res.is_err() || rd.checksum != 0 {
self.erase_file(header.name);
return Err(io::Error::new(io::ErrorKind::InvalidData, "TAP block checksum error"))
}
}
Ok(res)
}
fn retrieve_file<S: AsRef<[u8]>, W: Write>(
&self,
file_name: S,
mut wr: W
) -> io::Result<Option<(CatFileType, usize)>>
{
let mut len = 0;
let mut file_type = None;
for sector in self.file_sectors(file_name) {
let sector = sector.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
if sector.file_block_seq() == 0 {
file_type = sector.file_type().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
}
let record = sector.data_record();
wr.write_all(record)?;
len += record.len();
}
Ok(file_type.map(|ft| (ft, len)))
}
fn store_file<S: AsRef<[u8]>, R: Read>(
&mut self,
file_name: S,
is_save: bool,
mut rd: R
) -> io::Result<u8>
{
let file_name = file_name.as_ref();
if let Some((index, _)) = self.iter_with_indices().find(|(_,s)|
!s.is_free() && s.file_name_matches(file_name)) {
return Err(io::Error::new(io::ErrorKind::AlreadyExists,
MdrValidationError { index, description: "file name already exists" }))
}
let mut block_seq = 0;
let mut first_byte = 0u8;
if !rd.read_exact_or_none(slice::from_mut(&mut first_byte))? {
return Ok(0);
}
for sector in self.into_iter() {
if sector.is_free() {
let buf = sector.data_mut();
buf[0] = first_byte;
let len = 1 + rd.read_exact_or_to_end(&mut buf[1..])?;
sector.set_file_name(file_name);
sector.set_file_block_seq(block_seq);
block_seq += 1;
sector.set_file_block_len(len.try_into().unwrap());
sector.set_save_file_flag(is_save);
let is_last = !rd.read_exact_or_none(slice::from_mut(&mut first_byte))?;
sector.set_last_file_block(is_last);
sector.update_block_checksums();
if is_last {
return Ok(block_seq);
}
}
}
Err(io::Error::new(io::ErrorKind::WriteZero, "not enough free sectors to fit the whole file"))
}
fn erase_file<S: AsRef<[u8]>>(&mut self, file_name: S) -> u8 {
let file_name = file_name.as_ref();
let mut block_seq = 0;
for sector in self.into_iter() {
if !sector.is_free() && sector.file_name_matches(file_name) {
sector.erase();
block_seq += 1;
}
}
block_seq
}
fn file_sector_ids_unordered<S: AsRef<[u8]>>(
&self,
file_name: S
) -> FileSectorIdsUnordIter<'_>
{
let file_name = file_name.as_ref();
let mut name = [b' '; 10];
copy_name(&mut name, file_name);
FileSectorIdsUnordIter {
name,
iter: self.iter_with_indices()
}
}
fn file_sectors<S: AsRef<[u8]>>(
&self,
file_name: S
) -> FileSectorIter<'_>
{
let file_name = file_name.as_ref();
let mut name = [b' '; 10];
copy_name(&mut name, file_name);
let counter = self.count_formatted().try_into().unwrap();
FileSectorIter {
counter,
stop: !0,
block_seq: 0,
name,
iter: self.iter_with_indices().cycle()
}
}
fn catalog(&self) -> Result<Option<Catalog>, MdrValidationError> {
let name = if let Some(name) = self.catalog_name()? {
name.to_string()
}
else {
return Ok(None)
};
let mut files: HashMap<[u8;10], CatFile> = HashMap::new();
let mut sectors_free = 0u8;
for (index, sector) in self.iter_with_indices() {
if sector.is_free() {
sectors_free += 1;
}
else {
let meta = files.entry(*sector.file_name()).or_default();
if sector.file_block_seq() == 0 {
meta.copies += 1;
meta.file_type = sector.file_type()
.map_err(|e| MdrValidationError { index, description: e })?
.unwrap();
}
meta.blocks += 1;
meta.size += sector.file_block_len() as u32;
}
}
for meta in files.values_mut() {
if meta.copies == 0 {
return Err(MdrValidationError { index: u8::max_value(), description: "block 0 is missing" })
}
meta.size /= meta.copies as u32;
}
Ok(Some(Catalog {
name, files, sectors_free
}))
}
fn catalog_name(&self) -> Result<Option<Cow<'_, str>>, MdrValidationError> {
let mut header: Option<&[u8]> = None;
let mut seqs: HashSet<u8> = HashSet::new();
for (index, sector) in self.iter_with_indices() {
sector.validate().map_err(|description| MdrValidationError { index, description })?;
if let Some(name) = header {
if name != sector.catalog_name() {
return Err(MdrValidationError { index, description:
"catalog name is inconsistent" })
}
}
else {
header = Some(sector.catalog_name());
}
if !seqs.insert(sector.sector_seq()) {
return Err(MdrValidationError { index, description:
"at least two sectors have the same identification number" })
}
}
Ok(header.map(String::from_utf8_lossy))
}
fn validate_sectors(&self) -> Result<usize, MdrValidationError> {
let mut count = 0usize;
for (index, sector) in self.iter_with_indices() {
sector.validate().map_err(|description| MdrValidationError { index, description })?;
count += 1;
}
Ok(count)
}
fn count_sectors_in_use(&self) -> usize {
self.into_iter().filter(|sec| !sec.is_free()).count()
}
fn from_mdr<R: Read>(mut rd: R, max_sectors: usize) -> io::Result<Self> {
let mut sectors: Vec<Sector> = Vec::with_capacity(max_sectors);
let mut write_protect = false;
'sectors: loop {
let mut sector = Sector::default();
loop {
match rd.read_exact_or_to_end(&mut sector.head) {
Ok(0) => break 'sectors,
Ok(1) => {
write_protect = sector.head[0] != 0;
break 'sectors
}
Ok(HEAD_SIZE) => break,
Ok(..) => return Err(io::Error::new(io::ErrorKind::UnexpectedEof,
"failed to fill whole sector header")),
Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
if sectors.len() == MAX_USABLE_SECTORS {
return Err(io::Error::new(io::ErrorKind::InvalidData,
"only 254 sectors are supported"));
}
rd.read_exact(&mut sector.data)?;
sectors.push(sector);
}
if sectors.is_empty() {
return Err(io::Error::new(io::ErrorKind::InvalidData, "no sectors read"));
}
let max_sectors = max_sectors.max(sectors.len());
Ok(MicroCartridge::new_with_sectors(sectors, write_protect, max_sectors))
}
fn write_mdr<W: Write>(&self, mut wr: W) -> io::Result<usize> {
let mut bytes: usize = 0;
for sector in self.into_iter() {
wr.write_all(§or.head)?;
wr.write_all(§or.data)?;
bytes += sector.head.len() + sector.data.len();
}
if bytes == 0 {
return Ok(0)
}
wr.write_all(slice::from_ref(&self.is_write_protected().into()))?;
Ok(bytes + 1)
}
fn new_formatted<S: AsRef<[u8]>>(max_sectors: usize, catalog_name: S) -> Self {
let mut sectors = vec![Sector::default(); max_sectors.min(254)];
for (sector, seq) in sectors.iter_mut().rev().zip(1..254) {
sector.format(seq, catalog_name.as_ref());
}
MicroCartridge::new_with_sectors(sectors, false, max_sectors)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use rand::prelude::*;
use crate::tap;
#[test]
fn mdr_works() {
let mdr = MicroCartridge::default();
assert!(mdr.catalog_name().unwrap().is_none());
assert!(mdr.catalog().unwrap().is_none());
assert!(mdr.file_type("hello world").unwrap().is_none());
assert!(mdr.file_info("hello world").unwrap().is_none());
assert_eq!(mdr.count_sectors_in_use(), 0);
assert_eq!(mdr.max_sectors(), 256);
assert_eq!(mdr.count_formatted(), 0);
let mut mdr = MicroCartridge::new_formatted(10, "testing");
for i in 0..10 {
assert_eq!(mdr[i].is_free(), true);
assert_eq!(mdr[i].sector_seq(), 10 - i);
assert_eq!(mdr[i].catalog_name(), b"testing ");
assert_eq!(mdr[i].validate().unwrap(), ());
}
assert_eq!(mdr.count_sectors_in_use(), 0);
assert_eq!(mdr.max_sectors(), 10);
assert_eq!(mdr.count_formatted(), 10);
assert_eq!(mdr.catalog_name().unwrap().unwrap(), "testing ");
let catalog = mdr.catalog().unwrap().unwrap();
assert_eq!(catalog.name, "testing ");
assert_eq!(catalog.files, HashMap::<[u8;10],CatFile>::new());
assert_eq!(catalog.sectors_free, 10);
assert_eq!(mdr.store_file("hello world", false, io::Cursor::new([1,2,3,4,5])).unwrap(), 1);
assert_eq!(mdr.file_type("hello world").unwrap().unwrap(), CatFileType::Data);
assert_eq!(mdr.file_info("hello world").unwrap().unwrap(),
CatFile { size: 5, blocks: 1, copies: 1, file_type: CatFileType::Data});
assert_eq!(mdr.count_sectors_in_use(), 1);
assert_eq!(mdr.max_sectors(), 10);
assert_eq!(mdr.count_formatted(), 10);
let catalog = mdr.catalog().unwrap().unwrap();
assert_eq!(catalog.name, "testing ");
assert_eq!(catalog.files.len(), 1);
assert_eq!(catalog.files.get(b"hello worl").unwrap(), &CatFile {
size: 5,
blocks: 1,
copies: 1,
file_type: CatFileType::Data
});
assert_eq!(catalog.sectors_free, 9);
let mut wr = io::Cursor::new(Vec::new());
assert_eq!(mdr.retrieve_file("hello world", &mut wr).unwrap().unwrap(), (CatFileType::Data, 5));
assert_eq!(wr.get_ref(), &vec![1u8,2,3,4,5]);
assert_eq!(mdr.erase_file("hello world"), 1);
let catalog = mdr.catalog().unwrap().unwrap();
assert_eq!(catalog.name, "testing ");
assert_eq!(catalog.files.len(), 0);
assert_eq!(catalog.sectors_free, 10);
let mut big_file = vec![0u8;5000];
thread_rng().fill(&mut big_file[..]);
assert_eq!(mdr.store_file("big file", false, io::Cursor::new(&big_file[..])).unwrap(), 10);
let catalog = mdr.catalog().unwrap().unwrap();
assert_eq!(catalog.name, "testing ");
assert_eq!(catalog.files.len(), 1);
assert_eq!(catalog.files.get(b"big file ").unwrap(), &CatFile {
size: 5000,
blocks: 10,
copies: 1,
file_type: CatFileType::Data
});
assert_eq!(catalog.sectors_free, 0);
assert_eq!(mdr.file_type("big file").unwrap().unwrap(), CatFileType::Data);
assert_eq!(mdr.file_info("big file").unwrap().unwrap(),
CatFile { size: 5000, blocks: 10, copies: 1, file_type: CatFileType::Data});
let err = mdr.store_file("hello world", false, io::Cursor::new([1]));
assert!(err.is_err());
let err = err.err().unwrap();
assert_eq!(err.kind(), io::ErrorKind::WriteZero);
assert_eq!(err.to_string(), "not enough free sectors to fit the whole file");
let mut wr = io::Cursor::new(Vec::new());
assert_eq!(mdr.retrieve_file("big file", &mut wr).unwrap().unwrap(), (CatFileType::Data, 5000));
assert_eq!(wr.get_ref(), &big_file);
assert_eq!(mdr.erase_file("hello world"), 0);
assert_eq!(mdr.erase_file("big file"), 10);
let catalog = mdr.catalog().unwrap().unwrap();
assert_eq!(catalog.name, "testing ");
assert_eq!(catalog.files.len(), 0);
assert_eq!(catalog.sectors_free, 10);
let file = File::open("../resources/read_tap_test.tap").unwrap();
let mut tap_reader = tap::read_tap(file);
assert_eq!(mdr.file_from_tap_reader(tap_reader.by_ref()).unwrap(), 1);
assert_eq!(mdr.file_from_tap_reader(tap_reader.by_ref()).unwrap(), 1);
assert_eq!(mdr.file_from_tap_reader(tap_reader.by_ref()).unwrap(), 1);
let catalog = mdr.catalog().unwrap().unwrap();
assert_eq!(catalog.name, "testing ");
assert_eq!(catalog.files.len(), 3);
assert_eq!(catalog.sectors_free, 7);
assert_eq!(catalog.files.get(b"HelloWorld").unwrap(), &CatFile {
size: 9+6, blocks: 1, copies: 1, file_type: CatFileType::File(BlockType::Program)
});
assert_eq!(mdr.file_type("HelloWorld").unwrap().unwrap(), CatFileType::File(BlockType::Program));
assert_eq!(mdr.file_info("HelloWorld").unwrap().unwrap(),
CatFile { size: 9+6, blocks: 1, copies: 1, file_type: CatFileType::File(BlockType::Program)});
assert_eq!(catalog.files.get(b"a(10) ").unwrap(), &CatFile {
size: 9+53, blocks: 1, copies: 1, file_type: CatFileType::File(BlockType::NumberArray)
});
assert_eq!(mdr.file_type("a(10)").unwrap().unwrap(), CatFileType::File(BlockType::NumberArray));
assert_eq!(mdr.file_info("a(10)").unwrap().unwrap(),
CatFile { size: 9+53, blocks: 1, copies: 1, file_type: CatFileType::File(BlockType::NumberArray)});
assert_eq!(catalog.files.get(b"weekdays ").unwrap(), &CatFile {
size: 9+26, blocks: 1, copies: 1, file_type: CatFileType::File(BlockType::CharArray)
});
assert_eq!(mdr.file_type("weekdays").unwrap().unwrap(), CatFileType::File(BlockType::CharArray));
assert_eq!(mdr.file_info("weekdays").unwrap().unwrap(),
CatFile { size: 9+26, blocks: 1, copies: 1, file_type: CatFileType::File(BlockType::CharArray)});
let tap_data = io::Cursor::new(Vec::new());
let mut writer = tap::write_tap(tap_data).unwrap();
assert_eq!(mdr.file_to_tap_writer("HelloWorld", &mut writer).unwrap(), true);
assert_eq!(mdr.file_to_tap_writer("a(10)", &mut writer).unwrap(), true);
assert_eq!(mdr.file_to_tap_writer("weekdays", &mut writer).unwrap(), true);
let tap_file = std::fs::read("../resources/read_tap_test.tap").unwrap();
assert_eq!(tap_file, writer.into_inner().into_inner().into_inner());
}
}