spectrusty_formats/
mdr.rs

1/*
2    Copyright (C) 2020-2022  Rafal Michalski
3
4    This file is part of SPECTRUSTY, a Rust library for building emulators.
5
6    For the full copyright notice, see the lib.rs file.
7*/
8/*! **MDR** file format utilities.
9
10[Utilities][MicroCartridgeExt] in this module provide additional methods to the [MicroCartridge] type
11with abilities:
12
13* to inspect and manipulate sector data on the Microdrive file system level
14  (read, create, erase and list files from cartridges);
15* to extract files into and from the [**TAP**][super::tap] format;
16* to write and read [MicroCartridge] data to and from **MDR** file format.
17
18**MDR** files are data images of formatted sectors of [ZX Microdrive tape cartridges].
19
20The number of sectors may vary but it will never be a larger than 254.
21
22The last byte determines the write protection flag: if it's not `0` the cartridge should be protected.
23
24The structure of a single cartridge sector saved in the **MDR** file:
25
26| offset | size   | ROM var | description                                                                  |
27|------- |--------|---------|------------------------------------------------------------------------------|
28|      0 |      1 | HDFLAG  | Bit 0 if set indicates a header and should always be set.                    |
29|      1 |      1 | HDNUMB  | Sector number: starts from 254 down to 1.                                    |
30|      2 |      2 | -       | Unknown, probably padding.                                                   |
31|      4 |     10 | HDNAME  | Name of the cartridge with trailing spaces, repeated every formatted sector. |
32|     14 |      1 | HDCHK   | Checksum of the previous 14 bytes.                                           |
33|     15 |      1 | RECFLG  | Record flags, see below. Bit 0 should be always reset.                       |
34|     16 |      1 | RECNUM  | File block record number: starts at 0.                                       |
35|     17 |      2 | RECLEN  | File block data length (LSB first), less than or equal to 512.               |
36|     19 |     10 | RECNAM  | Name of the file with trailing spaces, repeated every record.                |
37|     29 |      1 | DESCHK  | Checksum of the previous 14 bytes.                                           |
38|     30 |    512 | CHDATA  | Record block data: 0..RECLEN.                                                |
39|    542 |      1 | DCHK    | Checksum of the previous 512 bytes, regardless of RECLEN value.              |
40
41Record flags (`RECFLG`):
42
43* Bit 0 is always reset to indicate a record block descriptor.
44* Bit 1 is set for the last record block (End Of File).
45* Bit 2 is set for regular files and reset if the record is part of a PRINT type file (a stream).
46
47The above structure and ROM variable names are from the book by Gianluca Carri
48["Spectrum Shadow ROM Disassembly"](https://spectrumcomputing.co.uk/entry/2000371/Book/Spectrum_Shadow_ROM_Disassembly).
49
50[ZX Microdrive tape cartridges]: https://en.wikipedia.org/wiki/ZX_Microdrive
51!*/
52use core::mem;
53use core::convert::{TryInto, TryFrom};
54use core::borrow::Borrow;
55use core::fmt;
56use core::iter::Cycle;
57use core::slice;
58use std::borrow::Cow;
59use std::io::{self, Read, Write, Seek};
60use std::collections::{HashMap, HashSet};
61
62use crate::ReadExactEx;
63use super::tap::{
64    BlockType, Header, TapChunkWriter, TapChunkReader, TapChunkRead, TapChunkInfo,
65    DATA_BLOCK_FLAG, array_name
66};
67pub use spectrusty_peripherals::storage::microdrives::{
68    MAX_USABLE_SECTORS, HEAD_SIZE,
69    Sector, MicroCartridge, MicroCartridgeIdSecIter};
70
71/// Checksum calculating routine used by Spectrum for Microdrive data.
72pub fn checksum<I: IntoIterator<Item=B>, B: Borrow<u8>>(iter: I) -> u8 {
73    iter.into_iter().fold(0, |acc, x| {
74        let (mut acc, carry) = acc.overflowing_add(*x.borrow());
75        acc = acc.wrapping_add(carry as u8);
76        if acc == u8::max_value() { 0 } else { acc }
77    })
78}
79
80/// Extends [MicroCartridge] with methods for reading and manipulating Microdrive's file system.
81pub trait MicroCartridgeExt: Sized {
82    /// Returns file meta data if the file with `file_name` exists.
83    fn file_info<S: AsRef<[u8]>>(&self, file_name: S) -> Result<Option<CatFile>, MdrValidationError>;
84    /// Returns a file type if the file with `file_name` exists.
85    fn file_type<S: AsRef<[u8]>>(&self, file_name: S) -> Result<Option<CatFileType>, MdrValidationError>;
86    /// Retrieves content of a file from a [MicroCartridge] and writes it to `wr` if the file with `file_name` exists.
87    /// Returns the type and the size of the file on success.
88    ///
89    /// If the file is a `SAVE *` type [file][CatFileType::File] the first 9 bytes of data constitute its
90    /// [file block header][MdrFileHeader].
91    fn retrieve_file<S: AsRef<[u8]>, W: Write>(&self, file_name: S, wr: W) -> io::Result<Option<(CatFileType, usize)>>;
92    /// Stores content read from `rd` as a new file on a [MicroCartridge].
93    /// Returns the number of newly occupied sectors on success.
94    ///
95    /// The `is_save` argument determines if the file is a binary file (`SAVE *`) or a data file (`OPEN #`).
96    /// In case `is_save` is `true` the first 9 bytes of the data read from `rd` must represent a
97    /// [file block header][MdrFileHeader].
98    ///
99    /// Returns an error if a file with the same name already exists or if there is not enough free sectors
100    /// to store the complete file.
101    ///
102    /// In case of an error of [io::ErrorKind::WriteZero] kind, you may delete the partial file data
103    /// with [MicroCartridgeExt::erase_file].
104    fn store_file<S: AsRef<[u8]>, R: Read>(&mut self, file_name: S, is_save: bool, rd: R) -> io::Result<u8>;
105    /// Marks all sectors (including copies and unclosed files) belonging to a provided `file_name` as free.
106    /// Returns the number of erased sectors.
107    fn erase_file<S: AsRef<[u8]>>(&mut self, file_name: S) -> u8;
108    /// Retrieves content of a binary file and writes it to a *TAP* chunk writer with
109    /// a proper *TAP* header.
110    ///
111    /// Returns `Ok(true)` if a `file_name` exists and the file was successfully written.
112    /// Returns `Ok(false)` if a `file_name` is missing or a file is not a binary (`SAVE *`) file.
113    fn file_to_tap_writer<S: AsRef<[u8]>, W: Write + Seek>(&self, file_name: S, wr: &mut TapChunkWriter<W>) -> io::Result<bool>;
114    /// Stores content read from a *TAP* chunk reader as a new file on a [MicroCartridge].
115    /// Returns the number of newly occupied sectors on success.
116    ///
117    /// The first *TAP* chunk must represent a proper [*TAP* header][Header] and the folowing chunk must be
118    /// a data block.
119    ///
120    /// Returns an error if a file with the same name already exists or if there is not enough free sectors
121    /// to store the complete file.
122    ///
123    /// In case of an error of [io::ErrorKind::WriteZero] kind, you may delete the partial file data
124    /// with [MicroCartridgeExt::erase_file].
125    fn file_from_tap_reader<R: Read + Seek>(&mut self, rd: &mut TapChunkReader<R>) -> io::Result<u8>;
126    /// Returns an iterator of sector indices with unordered blocks of the provided `file_name`.
127    ///
128    /// The same block numbers may be returned multiple times if there were multiple copies of the file.
129    fn file_sector_ids_unordered<S: AsRef<[u8]>>(&self, file_name: S) -> FileSectorIdsUnordIter<'_>;
130    /// Returns an iterator of sectors with ordered blocks belonging to the provided `file_name`.
131    ///
132    /// Does not return duplicate blocks if there are more than one copy of the file.
133    fn file_sectors<S: AsRef<[u8]>>(&self, file_name: S) -> FileSectorIter<'_>;
134    /// Validates formatted sectors and returns a catalog of files.
135    ///
136    /// Does not check the integrity of file blocks.
137    ///
138    /// Returns `Ok(None)` if there are no formatted sectors.
139    fn catalog(&self) -> Result<Option<Catalog>, MdrValidationError>;
140    /// Validates formatted sectors and returns a catalog name if all formatted sectors
141    /// contain the same header indicating a properly formatted cartridge.
142    ///
143    /// Does not check the integrity of file blocks.
144    ///
145    /// Returns `Ok(None)` if there are no formatted sectors.
146    fn catalog_name(&self) -> Result<Option<Cow<'_, str>>, MdrValidationError>;
147    /// Checks if each formatted sector has flags property set and if all checksums are valid.
148    fn validate_sectors(&self) -> Result<usize, MdrValidationError>;
149    /// Returns the number of sectors being used by file data.
150    fn count_sectors_in_use(&self) -> usize;
151    /// Reads the content of an `.mdr` file into the [MicroCartridge] sectors.
152    ///
153    /// The content of sectors is not being validated. The file can contain from 1 to 254 sectors.
154    /// If the file contains an additional byte, it is being read as a flag which is non-zero
155    /// if the cartridge is write protected.
156    ///
157    /// Provide the cartridge sector capacity as `max_sectors`. If the file contains more sectors
158    /// than `max_sectors` the cartridge sector capacity will equal to the number of sectors read.
159    ///
160    /// Use [MicroCartridgeExt::validate_sectors] to verify sector's content.
161    fn from_mdr<R: Read>(rd: R, max_sectors: usize) -> io::Result<Self>;
162    /// Writes all formatted sectors to an `.mdr` file using a provided writer.
163    ///
164    /// Returns the number of bytes written.
165    ///
166    /// Writes an additional byte as a flag which is non-zero if the cartridge is write protected.
167    ///
168    /// If there are no formatted sectors in the cartridge, doesn't write anything.
169    fn write_mdr<W: Write>(&self, wr: W) -> io::Result<usize>;
170    /// Creates a new instance of [MicroCartridge] with formatted sectors.
171    ///
172    /// # Panics
173    /// `max_sectors` must not be 0 and must not be greater than [MAX_SECTORS].
174    ///
175    /// [MAX_SECTORS]: spectrusty_peripherals::storage::microdrives::MAX_SECTORS
176    fn new_formatted<S: AsRef<[u8]>>(max_sectors: usize, catalog_name: S) -> Self;
177}
178
179/// Extends [MicroCartridge]'s [Sector] with methods for reading and manipulating Microdrive's file system.
180pub trait SectorExt {
181    /// Returns `true` if sector is free.
182    fn is_free(&self) -> bool;
183    /// Returns the name in sector's header.
184    fn catalog_name(&self) -> &[u8;10];
185    /// Returns the name in blocks's header.
186    fn file_name(&self) -> &[u8;10];
187    /// Returns `true` if the given `name` matches name in the blocks's header.
188    fn file_name_matches(&self, name: &[u8]) -> bool;
189    /// Returns a file type of this sector if the sector is occupied.
190    fn file_type(&self) -> Result<Option<CatFileType>, &'static str>;
191    /// Returns `HDNUMB` entry of the sector's header.
192    fn sector_seq(&self) -> u8;
193    /// Returns `RECNUM` entry of the blocks's header.
194    fn file_block_seq(&self) -> u8;
195    /// Returns `RECLEN` entry of the blocks's header.
196    fn file_block_len(&self) -> u16;
197    /// Returns an `EOF` flag's state of `RECFLG` entry of the blocks's header.
198    fn is_last_file_block(&self) -> bool;
199    /// Returns a `SAVE` flag's state of `RECFLG` entry of the blocks's header.
200    fn is_save_file(&self) -> bool;
201    /// Returns a reference to the data segment.
202    fn data(&self) -> &[u8];
203    /// Returns a mutable reference to the data segment.
204    fn data_mut(&mut self) -> &mut [u8];
205    /// Returns a reference to the data segment limited in size by the `RECLEN` entry.
206    fn data_record(&self) -> &[u8];
207    /// Changes the name in blocks's header.
208    fn set_file_name(&mut self, file_name: &[u8]);
209    /// Changes `RECNUM` entry of the blocks's header.
210    fn set_file_block_seq(&mut self, block_seq: u8);
211    /// Changes `RECLEN` entry of the blocks's header.
212    fn set_file_block_len(&mut self, len: u16);
213    /// Changes an `EOF` flag of `RECFLG` entry of the blocks's header.
214    fn set_last_file_block(&mut self, eof: bool);
215    /// Changes a `SAVE` flag of `RECFLG` entry of the blocks's header.
216    fn set_save_file_flag(&mut self, is_save: bool);
217    /// Updates checksums of a block segment.
218    fn update_block_checksums(&mut self);
219    /// Marks current sector as free. Updates block's header checksum.
220    fn erase(&mut self);
221    /// Formats this sector with the given sector number (1 - 254) and `catalog_name`.
222    ///
223    /// # Panics
224    /// If `seq` is not in [1, 254] range.
225    fn format(&mut self, seq: u8, catalog_name: &[u8]);
226    /// Validates the integrity of sector's content.
227    fn validate(&self) -> Result<(), &'static str>;
228    /// Returns a reference to the file header if this sector is the first sector of a `SAVE *` file.
229    fn file_header(&self) -> Result<Option<&MdrFileHeader>, &'static str>;
230    /// Creates a `TAP` [Header] if this sector is the first sector of a `SAVE *` file.
231    fn tap_header(&self) -> Result<Option<Header>, &'static str>;
232}
233
234
235/// An error returned when a [Sector]'s content is invalid.
236#[derive(Clone, Copy, Debug, PartialEq, Eq)]
237pub struct MdrValidationError {
238    /// An index of a faulty sector.
239    pub index: u8,
240    /// A description of an error.
241    pub description: &'static str
242}
243
244/// The structure returned by [MicroCartridgeExt::catalog] method.
245#[derive(Clone, Debug)]
246pub struct Catalog {
247    /// The name of the formatted cartridge.
248    pub name: String,
249    /// File names and their meta data.
250    pub files: HashMap<[u8;10], CatFile>,
251    /// A number of free sectors.
252    pub sectors_free: u8
253}
254
255/// [MicroCartridge]'s file meta data.
256#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
257pub struct CatFile {
258    /// Size of the file in bytes (single copy).
259    pub size: u32,
260    /// The number of sectors (blocks) the file occupies (all copies).
261    pub blocks: u8,
262    /// The number of copies of the file saved.
263    pub copies: u8,
264    /// The type of the file.
265    pub file_type: CatFileType
266}
267
268/// A [MicroCartridge]'s file type.
269#[derive(Clone, Copy, Debug, PartialEq, Eq)]
270pub enum CatFileType {
271    /// Represents a file created by the `OPEN #` command. Also called a `PRINT` file.
272    Data,
273    /// Represents a file created by the `SAVE *` command.
274    File(BlockType)
275}
276
277/// An iterator of sectors belonging to a single file.
278///
279/// It may scan all sectors, possibly more than once to find all the blocks belonging to a file in an ordered fashion.
280pub struct FileSectorIter<'a> {
281    counter: u8,
282    stop: u8,
283    block_seq: u8,
284    name: [u8; 10],
285    iter: Cycle<MicroCartridgeIdSecIter<'a>>
286}
287
288/// An iterator of sector indices and unordered block numbers belonging to a single file.
289pub struct FileSectorIdsUnordIter<'a> {
290    name: [u8; 10],
291    iter: MicroCartridgeIdSecIter<'a>
292}
293
294/// Instances of this type are returned by [FileSectorIdsUnordIter] iterator.
295#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
296pub struct SectorBlock {
297    /// A file sector index.
298    pub index: u8,
299    /// A file block number.
300    pub block_seq: u8
301}
302
303/// Binary (`SAVE *`) file's header.
304///
305/// Occupies the first 9 bytes of a first data block.
306///
307/// This is the equivalent of an [audio tape header][Header].
308#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
309#[repr(C,packed)]
310pub struct MdrFileHeader {
311    /// The type of the file this header represents.
312    pub block_type: BlockType,
313    /// Length of data.
314    pub length: [u8;2],
315    /// Start of data.
316    pub start: [u8;2],
317    /// Program length.
318    pub prog_length: [u8;2],
319    /// Program's starting line number.
320    pub line: [u8;2]
321}
322
323#[repr(C)]
324union MdrFileHeaderArray {
325    header: MdrFileHeader,
326    array: [u8; mem::size_of::<MdrFileHeader>()],
327}
328
329impl MdrFileHeader {
330    pub fn array_name(&self) -> char {
331        array_name(self.prog_length[0])
332    }
333
334    pub fn length(&self) -> u16 {
335        u16::from_le_bytes(self.length)
336    }
337
338    pub fn start(&self) -> u16 {
339        u16::from_le_bytes(self.start)
340    }
341
342    pub fn prog_length(&self) -> u16 {
343        u16::from_le_bytes(self.prog_length)
344    }
345
346    pub fn line(&self) -> u16 {
347        u16::from_le_bytes(self.line)
348    }
349
350    pub fn into_array(self) -> [u8; mem::size_of::<MdrFileHeader>()] {
351        unsafe { MdrFileHeaderArray { header: self }.array }
352    }
353}
354
355impl From<&'_ Header> for MdrFileHeader {
356    fn from(header: &Header) -> Self {
357        let block_type = header.block_type;
358        let length = header.length.to_le_bytes();
359        let mut start = header.par1;
360        let mut prog_length = [0, 0];
361        let mut line = [0, 0];
362        match block_type {
363            BlockType::Program => {
364                start = 0x5CCBu16.to_le_bytes();
365                line = header.par1;
366                prog_length = header.par2;
367            }
368            BlockType::NumberArray|BlockType::CharArray => {
369                start = 0x5CCBu16.to_le_bytes();
370                prog_length[0] = header.par1[1];
371            }
372            BlockType::Code => {}
373        }
374        MdrFileHeader { block_type, length, start, prog_length, line }
375    }
376}
377
378impl From<&'_ MdrFileHeader> for Header {
379    fn from(fbh: &MdrFileHeader) -> Self {
380        let length = u16::from_le_bytes(fbh.length);
381        match fbh.block_type {
382            BlockType::Program => {
383                let vars = u16::from_le_bytes(fbh.prog_length);
384                let line = u16::from_le_bytes(fbh.line);
385                Header::new_program(length).with_start(line).with_vars(vars)
386            }
387            BlockType::NumberArray => {
388                Header::new_number_array(length).with_array_name(fbh.array_name())
389            }
390            BlockType::CharArray => {
391                Header::new_char_array(length).with_array_name(fbh.array_name())
392            }
393            BlockType::Code => {
394                let start = u16::from_le_bytes(fbh.start);
395                Header::new_code(length).with_start(start)
396            }
397        }
398    }
399}
400
401impl TryFrom<&[u8]> for &MdrFileHeader {
402    type Error = &'static str;
403    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
404        let data: &[u8; mem::size_of::<MdrFileHeader>()] = data.try_into()
405                            .map_err(|_| "wrong size of slice for MdrFileHeader")?;
406        BlockType::try_from(data[0]).map_err(|_| "file type not recognized" )?;
407        let ptr = data as *const _ as  *const MdrFileHeader;
408        unsafe { Ok(&*ptr) }
409    }
410}
411
412const HDFLAG:  usize =   0; //  1
413const HDNUMB:  usize =   1; //  1
414const HDNAME:  usize =   4; // 10
415const HDCHK:   usize =  14; //  1
416
417const RECFLG:  usize =   0; //  1
418const RECNUM:  usize =   1; //  1
419const RECLEN:  usize =   2; //  2
420const RECNAM:  usize =   4; // 10
421const DESCHK:  usize =  14; //  1
422const HD_00:   usize =  15; //  1 File block type.
423// const HD_0B:   usize =  16; //  2 Length of data.
424// const HD_0D:   usize =  18; //  2 Start of data.
425// const HD_0F:   usize =  20; //  2 Program length. 
426// const HD_11:   usize =  22; //  2 Line number.
427const HD_SIZE: usize =   9; //
428const DCHK:    usize = 527; //  1
429
430const RECFLG_EOF:  u8 = 2;
431const RECFLG_SAVE: u8 = 4;
432
433const BLOCK_DATA_MAX: u16 = 512;
434
435fn copy_name(target: &mut [u8;10], name: &[u8]) {
436    let name_len = name.len().min(10);
437    target[..name_len].copy_from_slice(&name[..name_len]);
438    target[name_len..].iter_mut().for_each(|p| *p=b' ');
439}
440
441impl SectorExt for Sector {
442    fn is_free(&self) -> bool {
443        !self.is_last_file_block() && self.file_block_len() < BLOCK_DATA_MAX
444    }
445
446    fn catalog_name(&self) -> &[u8;10] {
447        self.head[HDNAME..HDNAME + 10].try_into().unwrap()
448    }
449
450    fn file_name(&self) -> &[u8;10] {
451        self.data[RECNAM..RECNAM + 10].try_into().unwrap()
452    }
453
454    fn file_name_matches(&self, name: &[u8]) -> bool {
455        let name = &name[0..name.len().min(10)];
456        if &self.data[RECNAM..RECNAM + name.len()] == name {
457            return self.data[RECNAM + name.len()..RECNAM + 10].iter().all(|p| *p==b' ')
458        }
459        false
460    }
461
462    fn file_type(&self) -> Result<Option<CatFileType>, &'static str> {
463        if self.is_free() {
464            return Ok(None)
465        }
466        Ok(Some(if self.is_save_file() {
467            CatFileType::File(BlockType::try_from(self.data[HD_00])
468                .map_err(|_| "file type not recognized" )?)
469        }
470        else {
471            CatFileType::Data
472        }))
473    }
474
475    fn sector_seq(&self) -> u8 {
476        self.head[HDNUMB]
477    }
478
479    fn file_block_seq(&self) -> u8 {
480        self.data[RECNUM]
481    }
482
483    fn file_block_len(&self) -> u16 {
484        let reclen = &self.data[RECLEN..RECLEN + 2];
485        u16::from_le_bytes(<[u8;2]>::try_from(reclen).unwrap())
486    }
487
488    fn is_last_file_block(&self) -> bool {
489        self.data[RECFLG] & RECFLG_EOF != 0
490    }
491
492    fn is_save_file(&self) -> bool {
493        self.data[RECFLG] & RECFLG_SAVE != 0
494    }
495
496    fn data(&self) -> &[u8] {
497        &self.data[DESCHK + 1..DCHK]
498    }
499
500    fn data_mut(&mut self) -> &mut [u8] {
501        &mut self.data[DESCHK + 1..DCHK]
502    }
503
504    fn data_record(&self) -> &[u8] {
505        let reclen = self.file_block_len() as usize;
506        &self.data[DESCHK + 1..DESCHK + 1 + reclen]
507    }
508
509    fn set_file_name(&mut self, file_name: &[u8]) {
510        let block_name = &mut self.data[RECNAM..RECNAM + 10];
511        copy_name(block_name.try_into().unwrap(), file_name);
512    }
513
514    fn set_file_block_seq(&mut self, block_seq: u8) {
515        self.data[RECNUM] = block_seq;
516    }
517
518    fn set_file_block_len(&mut self, len: u16) {
519        if len > BLOCK_DATA_MAX {
520            panic!("RECLEN must be less than 512");
521        }
522        let reclen = len.to_le_bytes();
523        self.data[RECLEN..RECLEN + 2].copy_from_slice(&reclen);
524    }
525
526    fn set_last_file_block(&mut self, eof: bool) {
527        if eof {
528            self.data[RECFLG] |= RECFLG_EOF;
529        }
530        else {
531            self.data[RECFLG] &= !RECFLG_EOF;
532        }
533    }
534
535    fn set_save_file_flag(&mut self, is_save: bool) {
536        if is_save {
537            self.data[RECFLG] |= RECFLG_SAVE;
538        }
539        else {
540            self.data[RECFLG] &= !RECFLG_SAVE;
541        }
542    }
543
544    fn update_block_checksums(&mut self) {
545        self.data[DESCHK] = checksum(&self.data[0..DESCHK]);
546        self.data[DCHK]   = checksum(&self.data[DESCHK + 1..DCHK])
547    }
548
549    fn erase(&mut self) {
550        self.data[RECFLG] = 0;
551        self.data[RECLEN..RECLEN + 2].iter_mut().for_each(|p| *p=0);
552        self.data[DESCHK] = checksum(&self.data[0..DESCHK]);
553    }
554
555    fn format(&mut self, seq: u8, catalog_name: &[u8]) {
556        if !(1..=245).contains(&seq) {
557            panic!("HDNUMB must be between 1 and 254");
558        }
559        self.head[HDFLAG] = 1;
560        self.head[HDNUMB] = seq;
561        let head_name = &mut self.head[HDNAME..HDNAME + 10];
562        copy_name(head_name.try_into().unwrap(), catalog_name);
563        self.head[HDCHK] = checksum(&self.head[0..HDCHK]);
564        self.erase();
565    }
566
567    fn validate(&self) -> Result<(), &'static str> {
568        if self.head[HDFLAG] & 1 != 1 {
569            return Err("bad sector header: HDFLAG bit 0 is reset");
570        }
571        if self.data[RECFLG] & 1 != 0 {
572            return Err("bad data block: RECFLG bit 0 is set");
573        }
574        if !(1..=254).contains(&self.head[HDNUMB]) {
575            return Err("bad sector header: HDNUMB is outside the allowed range: [1-254]");
576        }
577        if self.file_block_len() > BLOCK_DATA_MAX {
578            return Err("bad data block: RECLEN is larger than the sector size: 512");
579        }
580        if self.head[HDCHK] != checksum(&self.head[0..HDCHK]) {
581            return Err("bad sector header: HDCHK invalid checksum");
582        }
583        if self.data[DESCHK] != checksum(&self.data[0..DESCHK]) {
584            return Err("bad data block: DESCHK invalid header checksum");
585        }
586        if !self.is_free() &&
587           self.data[DCHK] != checksum(&self.data[DESCHK+1..DCHK])
588        {
589            return Err("bad data block: DESCHK invalid data checksum");
590        }
591        Ok(())
592    }
593
594    fn file_header(&self) -> Result<Option<&MdrFileHeader>, &'static str> {
595        if !self.is_free() && self.is_save_file() && self.file_block_seq() == 0 {
596            let mdrhd = <&MdrFileHeader>::try_from(&self.data[HD_00..HD_00 + HD_SIZE])?;
597            Ok(Some(mdrhd))
598        }
599        else {
600            Ok(None)
601        }
602
603    }
604
605    fn tap_header(&self) -> Result<Option<Header>, &'static str> {
606        self.file_header().map(|m|
607            m.map(|hd| Header::from(hd).with_name(&self.data[RECNAM..RECNAM + 10]))
608        )
609    }
610}
611
612impl Default for CatFileType {
613    fn default() -> Self {
614        CatFileType::Data
615    }
616}
617
618impl fmt::Display for Catalog {
619    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
620        writeln!(f, "{:10}:", self.name)?;
621        for (name, file) in self.files.iter() {
622            writeln!(f, "  {:10} {}", String::from_utf8_lossy(name), file)?;
623        }
624        writeln!(f, "free: {} sec. {} bytes",
625                        self.sectors_free,
626                        self.sectors_free as u32 * BLOCK_DATA_MAX as u32)
627    }
628}
629
630impl fmt::Display for CatFile {
631    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
632        write!(f, "{} {:5} bytes {:2} blk.", self.file_type, self.size, self.blocks)?;
633        if self.copies != 1 {
634            write!(f, " {} copies", self.copies)?;
635        }
636        Ok(())
637    }
638}
639
640impl fmt::Display for CatFileType {
641    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
642        match self {
643            CatFileType::Data => f.write_str("Data"),
644            CatFileType::File(bt) => bt.fmt(f),
645        }
646    }
647}
648
649impl std::error::Error for MdrValidationError {}
650
651impl fmt::Display for MdrValidationError {
652    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
653        write!(f, "MDR validation error in sector #{}: {}", self.index, self.description)
654    }
655}
656
657impl<'a> Iterator for FileSectorIter<'a> {
658    type Item = Result<&'a Sector, MdrValidationError>;
659
660    fn next(&mut self) -> Option<Self::Item> {
661        if self.counter == 0 {
662            return None
663        }
664        for (index, sector) in self.iter.by_ref() {
665            if self.stop == index {
666                self.counter = 0;
667            }
668            else {
669                self.counter -= 1;
670                if !sector.is_free() && sector.file_name() == &self.name
671                   && sector.file_block_seq() == self.block_seq
672                {
673                    if sector.is_last_file_block() {
674                        self.counter = 0;
675                    }
676                    else {
677                        self.stop = index;
678                        self.counter = !0;
679                        self.block_seq += 1;
680                    }
681                    return Some(Ok(sector))
682                }
683            }
684            if self.counter == 0 {
685                if self.block_seq != 0 {
686                    return Some(Err(MdrValidationError { index, description:
687                                    "missing end of file block" }))
688                }
689                break
690            }
691        }
692        None
693    }
694}
695
696impl<'a> Iterator for FileSectorIdsUnordIter<'a> {
697    type Item = SectorBlock;
698
699    fn next(&mut self) -> Option<Self::Item> {
700        for (index, sector) in self.iter.by_ref() {
701            if !sector.is_free() && sector.file_name() == &self.name {
702                let block_seq = sector.file_block_seq();
703                return Some(SectorBlock { index, block_seq })
704            }
705        }
706        None
707    }
708}
709
710impl MicroCartridgeExt for MicroCartridge {
711    fn file_info<S: AsRef<[u8]>>(
712            &self,
713            file_name: S
714        ) -> Result<Option<CatFile>, MdrValidationError>
715    {
716        let file_name = file_name.as_ref();
717        let mut meta = CatFile::default();
718        for (index, sector) in self.iter_with_indices() {
719            if !sector.is_free() && sector.file_name_matches(file_name) {
720                if sector.file_block_seq() == 0 {
721                    meta.copies += 1;
722                    meta.file_type = sector.file_type()
723                        .map_err(|e| MdrValidationError { index, description: e })?
724                        .unwrap();
725                }
726                meta.blocks += 1;
727                meta.size += sector.file_block_len() as u32;
728            }
729        }
730        if meta.copies != 0 {
731            meta.size /= meta.copies as u32;
732            Ok(Some(meta))
733        }
734        else {
735            Ok(None)
736        }
737    }
738
739    fn file_type<S: AsRef<[u8]>>(
740            &self,
741            file_name: S
742        ) -> Result<Option<CatFileType>, MdrValidationError>
743    {
744        let file_name = file_name.as_ref();
745        for (index, sector) in self.iter_with_indices() {
746            if !sector.is_free() && sector.file_block_seq() == 0 && sector.file_name_matches(file_name) {
747                return sector.file_type().map_err(|e| MdrValidationError { index, description: e })
748            }
749        }
750        Ok(None)
751    }
752
753    fn file_to_tap_writer<S: AsRef<[u8]>, W: Write + Seek>(
754            &self,
755            file_name: S,
756            wr: &mut TapChunkWriter<W>
757        ) -> io::Result<bool>
758    {
759        let mut sector_iter = self.file_sectors(file_name);
760        #[allow(clippy::never_loop)]
761        let mut tran = loop {
762            if let Some(sector) = sector_iter.next() {
763                let sector = sector.unwrap();
764                if let Some(header) = sector.tap_header()
765                                      .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?
766                {
767                    wr.write_header(&header)?;
768                    let mut tran = wr.begin()?;
769                    tran.write_all(slice::from_ref(&DATA_BLOCK_FLAG))?;
770                    tran.write_all(&sector.data_record()[HD_SIZE..])?;
771                    break tran
772                }
773            }
774            return Ok(false)
775        };
776        for sector in sector_iter {
777            let sector = sector.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
778            let record = sector.data_record();
779            tran.write_all(record)?;
780        }
781        tran.commit(true)?;
782        Ok(true)
783    }
784
785    fn file_from_tap_reader<R: Read + Seek>(&mut self, rd: &mut TapChunkReader<R>) -> io::Result<u8> {
786        if rd.chunk_limit() == 0 {
787            rd.next_chunk()?;
788        }
789        let header = if let TapChunkInfo::Head(header) = TapChunkInfo::try_from(rd.get_mut())? {
790            header
791        }
792        else {
793            return Err(io::Error::new(io::ErrorKind::InvalidData, "not a TAP header"))
794        };
795        if let Some(chunk_size) = rd.next_chunk()? {
796            if chunk_size < 2 || header.length != chunk_size - 2 {
797                return Err(io::Error::new(io::ErrorKind::InvalidData, "not a TAP block"))
798            }
799            let mut flag = 0u8;
800            rd.read_exact(slice::from_mut(&mut flag))?;
801            if flag != DATA_BLOCK_FLAG {
802                return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid TAP block flag"))
803            }
804        }
805        else {
806            return Err(io::Error::new(io::ErrorKind::InvalidData, "missing TAP chunk"))
807        }
808        let fbheader = MdrFileHeader::from(&header).into_array();
809        let hdrd = io::Cursor::new(fbheader).chain(rd.take(header.length as u64));
810        let res = self.store_file(header.name, true, hdrd)?;
811        {
812            let mut checksum = 0u8;
813            let res = rd.read_exact(slice::from_mut(&mut checksum));
814            if res.is_err() || rd.checksum != 0 {
815                self.erase_file(header.name);
816                return Err(io::Error::new(io::ErrorKind::InvalidData, "TAP block checksum error"))
817            }
818        }
819        Ok(res)
820    }
821
822    fn retrieve_file<S: AsRef<[u8]>, W: Write>(
823            &self,
824            file_name: S,
825            mut wr: W
826        ) -> io::Result<Option<(CatFileType, usize)>>
827    {
828        let mut len = 0;
829        let mut file_type = None;
830        for sector in self.file_sectors(file_name) {
831            let sector = sector.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
832            if sector.file_block_seq() == 0 {
833                file_type = sector.file_type().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
834            }
835            let record = sector.data_record();
836            wr.write_all(record)?;
837            len += record.len();
838        }
839        Ok(file_type.map(|ft| (ft, len)))
840    }
841
842    fn store_file<S: AsRef<[u8]>, R: Read>(
843            &mut self,
844            file_name: S,
845            is_save: bool,
846            mut rd: R
847        ) -> io::Result<u8>
848    {
849        let file_name = file_name.as_ref();
850        if let Some((index, _)) = self.iter_with_indices().find(|(_,s)|
851                                    !s.is_free() && s.file_name_matches(file_name)) {
852            return Err(io::Error::new(io::ErrorKind::AlreadyExists,
853                        MdrValidationError { index, description: "file name already exists" }))
854        }
855        let mut block_seq = 0;
856        let mut first_byte = 0u8;
857        if !rd.read_exact_or_none(slice::from_mut(&mut first_byte))? {
858            return Ok(0);
859        }
860        for sector in self.into_iter() {
861            if sector.is_free() {
862                let buf = sector.data_mut();
863                buf[0] = first_byte;
864                let len = 1 + rd.read_exact_or_to_end(&mut buf[1..])?;
865                sector.set_file_name(file_name);
866                sector.set_file_block_seq(block_seq);
867                block_seq += 1;
868                sector.set_file_block_len(len.try_into().unwrap());
869                sector.set_save_file_flag(is_save);
870                let is_last = !rd.read_exact_or_none(slice::from_mut(&mut first_byte))?;
871                sector.set_last_file_block(is_last);
872                sector.update_block_checksums();
873                if is_last {
874                    return Ok(block_seq);
875                }
876            }
877        }
878        Err(io::Error::new(io::ErrorKind::WriteZero, "not enough free sectors to fit the whole file"))
879    }
880
881    fn erase_file<S: AsRef<[u8]>>(&mut self, file_name: S) -> u8 {
882        let file_name = file_name.as_ref();
883        let mut block_seq = 0;
884        for sector in self.into_iter() {
885            if !sector.is_free() && sector.file_name_matches(file_name) {
886                sector.erase();
887                block_seq += 1;
888            }
889        }
890        block_seq
891    }
892
893    fn file_sector_ids_unordered<S: AsRef<[u8]>>(
894            &self,
895            file_name: S
896        ) -> FileSectorIdsUnordIter<'_>
897    {
898        let file_name = file_name.as_ref();
899        let mut name = [b' '; 10];
900        copy_name(&mut name, file_name);
901        FileSectorIdsUnordIter {
902            name,
903            iter: self.iter_with_indices()
904        }
905    }
906
907    fn file_sectors<S: AsRef<[u8]>>(
908            &self,
909            file_name: S
910        ) -> FileSectorIter<'_>
911    {
912        let file_name = file_name.as_ref();
913        let mut name = [b' '; 10];
914        copy_name(&mut name, file_name);
915        let counter = self.count_formatted().try_into().unwrap();
916        FileSectorIter {
917            counter,
918            stop: !0,
919            block_seq: 0,
920            name,
921            iter: self.iter_with_indices().cycle()
922        }
923    }
924
925    fn catalog(&self) -> Result<Option<Catalog>, MdrValidationError> {
926        let name = if let Some(name) = self.catalog_name()? {
927            name.to_string()
928        }
929        else {
930            return Ok(None)
931        };
932        let mut files: HashMap<[u8;10], CatFile> = HashMap::new();
933        let mut sectors_free = 0u8;
934        for (index, sector) in self.iter_with_indices() {
935            if sector.is_free() {
936                sectors_free += 1;
937            }
938            else {
939                let meta = files.entry(*sector.file_name()).or_default();
940                if sector.file_block_seq() == 0 {
941                    meta.copies += 1;
942                    meta.file_type = sector.file_type()
943                        .map_err(|e| MdrValidationError { index, description: e })?
944                        .unwrap();
945                }
946                meta.blocks += 1;
947                meta.size += sector.file_block_len() as u32;
948            }
949        }
950        for meta in files.values_mut() {
951            if meta.copies == 0 {
952                return Err(MdrValidationError { index: u8::max_value(), description: "block 0 is missing" })
953            }
954            meta.size /= meta.copies as u32;
955        }
956        Ok(Some(Catalog {
957            name, files, sectors_free
958        }))
959    }
960
961    fn catalog_name(&self) -> Result<Option<Cow<'_, str>>, MdrValidationError> {
962        let mut header: Option<&[u8]> = None;
963        let mut seqs: HashSet<u8> = HashSet::new();
964        for (index, sector) in self.iter_with_indices() {
965            sector.validate().map_err(|description| MdrValidationError { index, description })?;
966            if let Some(name) = header {
967                if name != sector.catalog_name() {
968                    return Err(MdrValidationError { index, description:
969                        "catalog name is inconsistent" })
970                }
971            }
972            else {
973                header = Some(sector.catalog_name());
974            }
975            if !seqs.insert(sector.sector_seq()) {
976                return Err(MdrValidationError { index, description:
977                    "at least two sectors have the same identification number" })
978            }
979        }
980        Ok(header.map(String::from_utf8_lossy))
981    }
982
983    fn validate_sectors(&self) -> Result<usize, MdrValidationError> {
984        let mut count = 0usize;
985        for (index, sector) in self.iter_with_indices() {
986            sector.validate().map_err(|description| MdrValidationError { index, description })?;
987            count += 1;
988        }
989        Ok(count)
990    }
991
992    fn count_sectors_in_use(&self) -> usize {
993        self.into_iter().filter(|sec| !sec.is_free()).count()
994    }
995
996    fn from_mdr<R: Read>(mut rd: R, max_sectors: usize) -> io::Result<Self> {
997        let mut sectors: Vec<Sector> = Vec::with_capacity(max_sectors);
998        let mut write_protect = false;
999        'sectors: loop {
1000            let mut sector = Sector::default();
1001            loop {
1002                match rd.read_exact_or_to_end(&mut sector.head) {
1003                    Ok(0) => break 'sectors,
1004                    Ok(1) => {
1005                        write_protect = sector.head[0] != 0;
1006                        break 'sectors
1007                    }
1008                    Ok(HEAD_SIZE) => break,
1009                    Ok(..) => return Err(io::Error::new(io::ErrorKind::UnexpectedEof,
1010                                                "failed to fill whole sector header")),
1011                    Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
1012                    Err(e) => return Err(e),
1013                }
1014            }
1015            if sectors.len() == MAX_USABLE_SECTORS {
1016                return Err(io::Error::new(io::ErrorKind::InvalidData,
1017                                            "only 254 sectors are supported"));
1018            }
1019            rd.read_exact(&mut sector.data)?;
1020            sectors.push(sector);
1021        }
1022        if sectors.is_empty() {
1023            return Err(io::Error::new(io::ErrorKind::InvalidData, "no sectors read"));
1024        }
1025        let max_sectors = max_sectors.max(sectors.len());
1026        Ok(MicroCartridge::new_with_sectors(sectors, write_protect, max_sectors))
1027    }
1028
1029    fn write_mdr<W: Write>(&self, mut wr: W) -> io::Result<usize> {
1030        let mut bytes: usize = 0;
1031        for sector in self.into_iter() {
1032            wr.write_all(&sector.head)?;
1033            wr.write_all(&sector.data)?;
1034            bytes += sector.head.len() + sector.data.len();
1035        }
1036        if bytes == 0 {
1037            return Ok(0)
1038        }
1039        wr.write_all(slice::from_ref(&self.is_write_protected().into()))?;
1040        Ok(bytes + 1)
1041    }
1042
1043    fn new_formatted<S: AsRef<[u8]>>(max_sectors: usize, catalog_name: S) -> Self {
1044        let mut sectors = vec![Sector::default(); max_sectors.min(254)];
1045        for (sector, seq) in sectors.iter_mut().rev().zip(1..254) {
1046            sector.format(seq, catalog_name.as_ref());
1047        }
1048        MicroCartridge::new_with_sectors(sectors, false, max_sectors)
1049    }
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054    use super::*;
1055    use std::fs::File;
1056    use rand::prelude::*;
1057    use crate::tap;
1058
1059    #[test]
1060    fn mdr_works() {
1061        let mdr = MicroCartridge::default();
1062        assert!(mdr.catalog_name().unwrap().is_none());
1063        assert!(mdr.catalog().unwrap().is_none());
1064        assert!(mdr.file_type("hello world").unwrap().is_none());
1065        assert!(mdr.file_info("hello world").unwrap().is_none());
1066        assert_eq!(mdr.count_sectors_in_use(), 0);
1067        assert_eq!(mdr.max_sectors(), 256);
1068        assert_eq!(mdr.count_formatted(), 0);
1069        let mut mdr = MicroCartridge::new_formatted(10, "testing");
1070        for i in 0..10 {
1071            assert_eq!(mdr[i].is_free(), true);
1072            assert_eq!(mdr[i].sector_seq(), 10 - i);
1073            assert_eq!(mdr[i].catalog_name(), b"testing   ");
1074            assert_eq!(mdr[i].validate().unwrap(), ());
1075        }
1076        assert_eq!(mdr.count_sectors_in_use(), 0);
1077        assert_eq!(mdr.max_sectors(), 10);
1078        assert_eq!(mdr.count_formatted(), 10);
1079        assert_eq!(mdr.catalog_name().unwrap().unwrap(), "testing   ");
1080        let catalog = mdr.catalog().unwrap().unwrap();
1081        assert_eq!(catalog.name, "testing   ");
1082        assert_eq!(catalog.files, HashMap::<[u8;10],CatFile>::new());
1083        assert_eq!(catalog.sectors_free, 10);
1084        assert_eq!(mdr.store_file("hello world", false, io::Cursor::new([1,2,3,4,5])).unwrap(), 1);
1085        assert_eq!(mdr.file_type("hello world").unwrap().unwrap(), CatFileType::Data);
1086        assert_eq!(mdr.file_info("hello world").unwrap().unwrap(),
1087                CatFile { size: 5, blocks: 1, copies: 1, file_type: CatFileType::Data});
1088        assert_eq!(mdr.count_sectors_in_use(), 1);
1089        assert_eq!(mdr.max_sectors(), 10);
1090        assert_eq!(mdr.count_formatted(), 10);
1091        let catalog = mdr.catalog().unwrap().unwrap();
1092        assert_eq!(catalog.name, "testing   ");
1093        assert_eq!(catalog.files.len(), 1);
1094        assert_eq!(catalog.files.get(b"hello worl").unwrap(), &CatFile {
1095            size: 5,
1096            blocks: 1,
1097            copies: 1,
1098            file_type: CatFileType::Data
1099        });
1100        assert_eq!(catalog.sectors_free, 9);
1101        let mut wr = io::Cursor::new(Vec::new());
1102        assert_eq!(mdr.retrieve_file("hello world", &mut wr).unwrap().unwrap(), (CatFileType::Data, 5));
1103        assert_eq!(wr.get_ref(), &vec![1u8,2,3,4,5]);
1104        assert_eq!(mdr.erase_file("hello world"), 1);
1105        let catalog = mdr.catalog().unwrap().unwrap();
1106        assert_eq!(catalog.name, "testing   ");
1107        assert_eq!(catalog.files.len(), 0);
1108        assert_eq!(catalog.sectors_free, 10);
1109        let mut big_file = vec![0u8;5000];
1110        thread_rng().fill(&mut big_file[..]);
1111        assert_eq!(mdr.store_file("big file", false, io::Cursor::new(&big_file[..])).unwrap(), 10);
1112        let catalog = mdr.catalog().unwrap().unwrap();
1113        assert_eq!(catalog.name, "testing   ");
1114        assert_eq!(catalog.files.len(), 1);
1115        assert_eq!(catalog.files.get(b"big file  ").unwrap(), &CatFile {
1116            size: 5000,
1117            blocks: 10,
1118            copies: 1,
1119            file_type: CatFileType::Data
1120        });
1121        assert_eq!(catalog.sectors_free, 0);
1122        assert_eq!(mdr.file_type("big file").unwrap().unwrap(), CatFileType::Data);
1123        assert_eq!(mdr.file_info("big file").unwrap().unwrap(),
1124                CatFile { size: 5000, blocks: 10, copies: 1, file_type: CatFileType::Data});
1125        let err = mdr.store_file("hello world", false, io::Cursor::new([1]));
1126        assert!(err.is_err());
1127        let err = err.err().unwrap();
1128        assert_eq!(err.kind(), io::ErrorKind::WriteZero);
1129        assert_eq!(err.to_string(), "not enough free sectors to fit the whole file");
1130        let mut wr = io::Cursor::new(Vec::new());
1131        assert_eq!(mdr.retrieve_file("big file", &mut wr).unwrap().unwrap(), (CatFileType::Data, 5000));
1132        assert_eq!(wr.get_ref(), &big_file);
1133        assert_eq!(mdr.erase_file("hello world"), 0);
1134        assert_eq!(mdr.erase_file("big file"), 10);
1135        let catalog = mdr.catalog().unwrap().unwrap();
1136        assert_eq!(catalog.name, "testing   ");
1137        assert_eq!(catalog.files.len(), 0);
1138        assert_eq!(catalog.sectors_free, 10);
1139
1140        let file = File::open("../resources/read_tap_test.tap").unwrap();
1141        let mut tap_reader = tap::read_tap(file);
1142        assert_eq!(mdr.file_from_tap_reader(tap_reader.by_ref()).unwrap(), 1);
1143        assert_eq!(mdr.file_from_tap_reader(tap_reader.by_ref()).unwrap(), 1);
1144        assert_eq!(mdr.file_from_tap_reader(tap_reader.by_ref()).unwrap(), 1);
1145        let catalog = mdr.catalog().unwrap().unwrap();
1146        assert_eq!(catalog.name, "testing   ");
1147        assert_eq!(catalog.files.len(), 3);
1148        assert_eq!(catalog.sectors_free, 7);
1149        assert_eq!(catalog.files.get(b"HelloWorld").unwrap(), &CatFile {
1150            size: 9+6, blocks: 1, copies: 1, file_type: CatFileType::File(BlockType::Program)
1151        });
1152        assert_eq!(mdr.file_type("HelloWorld").unwrap().unwrap(), CatFileType::File(BlockType::Program));
1153        assert_eq!(mdr.file_info("HelloWorld").unwrap().unwrap(),
1154                CatFile { size: 9+6, blocks: 1, copies: 1, file_type: CatFileType::File(BlockType::Program)});
1155        assert_eq!(catalog.files.get(b"a(10)     ").unwrap(), &CatFile {
1156            size: 9+53, blocks: 1, copies: 1, file_type: CatFileType::File(BlockType::NumberArray)
1157        });
1158        assert_eq!(mdr.file_type("a(10)").unwrap().unwrap(), CatFileType::File(BlockType::NumberArray));
1159        assert_eq!(mdr.file_info("a(10)").unwrap().unwrap(),
1160                CatFile { size: 9+53, blocks: 1, copies: 1, file_type: CatFileType::File(BlockType::NumberArray)});
1161        assert_eq!(catalog.files.get(b"weekdays  ").unwrap(), &CatFile {
1162            size: 9+26, blocks: 1, copies: 1, file_type: CatFileType::File(BlockType::CharArray)
1163        });
1164        assert_eq!(mdr.file_type("weekdays").unwrap().unwrap(), CatFileType::File(BlockType::CharArray));
1165        assert_eq!(mdr.file_info("weekdays").unwrap().unwrap(),
1166                CatFile { size: 9+26, blocks: 1, copies: 1, file_type: CatFileType::File(BlockType::CharArray)});
1167        let tap_data = io::Cursor::new(Vec::new());
1168        let mut writer = tap::write_tap(tap_data).unwrap();
1169        assert_eq!(mdr.file_to_tap_writer("HelloWorld", &mut writer).unwrap(), true);
1170        assert_eq!(mdr.file_to_tap_writer("a(10)", &mut writer).unwrap(), true);
1171        assert_eq!(mdr.file_to_tap_writer("weekdays", &mut writer).unwrap(), true);
1172        let tap_file = std::fs::read("../resources/read_tap_test.tap").unwrap();
1173        assert_eq!(tap_file, writer.into_inner().into_inner().into_inner());
1174    }
1175}