Skip to main content

image_rider/disk_format/apple/
catalog.rs

1//! Catalog structures and files for an Apple DOS 3.3 disk
2use log::debug;
3
4use nom::error::ErrorKind;
5use nom::{Err, IResult};
6
7use nom::bytes::complete::take;
8use nom::multi::count;
9use nom::number::complete::{le_u16, le_u8};
10
11use std::{
12    collections::HashMap,
13    fmt::{Display, Formatter, Result},
14    string::FromUtf8Error,
15};
16
17use crate::serialize::{little_endian_word_to_bytes, Serializer};
18
19/// Different file types
20#[derive(Clone, Copy, Debug)]
21pub enum FileType {
22    /// Text file
23    Text = 0,
24    /// Integer BASIC file
25    IntegerBasic = 1,
26    /// AppleSoft BASIC file
27    AppleSoftBasic = 2,
28    /// Binary file
29    Binary = 4,
30    /// S Type file
31    SType = 8,
32    /// Relocatable Object Module file
33    RelocatableObjectModule = 10,
34    /// A Type file
35    AType = 20,
36    /// B Type file
37    BType = 40,
38    /// Unknown file type
39    Unknown,
40}
41
42/// Display a FileType as a single character
43impl Display for FileType {
44    fn fmt(&self, f: &mut Formatter) -> Result {
45        match self {
46            FileType::Text => write!(f, "T"),
47            FileType::IntegerBasic => write!(f, "I"),
48            FileType::AppleSoftBasic => write!(f, "A"),
49            FileType::Binary => write!(f, "B"),
50            FileType::SType => write!(f, "S"),
51            FileType::RelocatableObjectModule => write!(f, "R"),
52            FileType::AType => write!(f, "AT"),
53            FileType::BType => write!(f, "BT"),
54            FileType::Unknown => write!(f, "U"),
55        }
56    }
57}
58
59/// A file entry
60#[derive(Clone, Copy, Debug)]
61pub struct FileEntry<'a> {
62    /// Track index  of the start of the file
63    pub track_of_first_track_sector_list_sector: u8,
64    /// Sector index of the start of the file
65    pub sector_of_first_track_sector_list_sector: u8,
66
67    /// The file type
68    pub file_type: FileType,
69
70    /// Whether the file is locked
71    pub locked: bool,
72
73    /// The file name, bytes 0x03-0x20 (30 bytes)
74    pub file_name: &'a [u8],
75
76    /// The file length in number of sectors
77    pub file_length_in_sectors: u16,
78}
79
80/// A File
81/// Has a filename, Track / Sector List and data for the file.
82pub struct File<'a> {
83    /// The track sector lists for this file
84    track_sector_lists: TrackSectorLists<'a>,
85
86    /// The file data
87    pub data: Vec<u8>,
88}
89
90impl Display for File<'_> {
91    fn fmt(&self, f: &mut Formatter) -> Result {
92        for tsl in &self.track_sector_lists {
93            writeln!(f, "track_sector_list: {}", tsl)?;
94        }
95        writeln!(f, "length of data: {}", self.data.len())
96    }
97}
98
99/// Files are a collection of File objects indexed by filename.
100pub type Files<'a> = HashMap<String, File<'a>>;
101
102/// A track/sector list.
103/// Each file has an associated track/sector
104/// list.  There may be more track/sector lists.
105#[derive(Clone)]
106pub struct TrackSectorList<'a> {
107    /// Reserved byte
108    pub reserved: u8,
109    /// The track number of the next track/sector list, or None if
110    /// there is none.
111    pub track_number_of_next_sector: Option<u8>,
112    /// The sector number of the next track/sector list, or None if
113    /// there is none.
114    pub sector_number_of_next_sector: Option<u8>,
115    /// Two reserved bytes
116    pub reserved_2: &'a [u8],
117
118    /// Sector offset in file of the first sector described by this list
119    /// Two bytes
120    pub sector_offset_in_file: &'a [u8],
121
122    /// 5 reserved bytes
123    pub reserved_3: &'a [u8],
124
125    /// Vector of TrackSectorPairs for this TrackSectorList
126    pub track_sector_pairs: TrackSectorPairs, // Vec<TrackSectorPair>,
127}
128
129/// Display a FileType as a single character
130impl Display for TrackSectorList<'_> {
131    fn fmt(&self, f: &mut Formatter) -> Result {
132        write!(f, "reserved: {}", self.reserved)?;
133        match self.track_number_of_next_sector {
134            Some(x) => {
135                writeln!(f, "track_number_of_next_sector: 0x{:02X}", x)?;
136            }
137            None => {
138                writeln!(f, "track_number_of_next_sector: None")?;
139            }
140        }
141        match self.sector_number_of_next_sector {
142            Some(x) => {
143                writeln!(f, "sector_number_of_next_sector: 0x{:02X}, ", x)?;
144            }
145            None => {
146                writeln!(f, "sector_number_of_next_sector: None")?;
147            }
148        }
149        write!(f, "reserved_2: {:?}", self.reserved_2)?;
150        writeln!(f, "Track Sector Pairs:")?;
151        for tsp in &self.track_sector_pairs {
152            writeln!(f, "track_sector_pair: {}", tsp)?;
153        }
154        writeln!(f)
155    }
156}
157
158impl<'a> Serializer<'a> for TrackSectorList<'a> {
159    fn as_vec(&'a self) -> std::result::Result<Vec<u8>, crate::error::Error> {
160        let mut bytes = Vec::new();
161
162        bytes.push(self.reserved);
163        // TODO: Test this
164        if let Some(track_number) = self.track_number_of_next_sector {
165            bytes.push(track_number);
166        } else {
167            bytes.push(0);
168        }
169        if let Some(sector_number) = self.sector_number_of_next_sector {
170            bytes.push(sector_number);
171        } else {
172            bytes.push(0);
173        }
174
175        bytes.append(&mut self.reserved_2.to_vec());
176        bytes.append(&mut self.sector_offset_in_file.to_vec());
177        bytes.append(&mut self.reserved_3.to_vec());
178        bytes.append(&mut self.track_sector_pairs.as_vec().unwrap());
179
180        Ok(bytes)
181    }
182}
183
184/// A vector of track sector lists
185// #[derive(Debug)]
186pub type TrackSectorLists<'a> = Vec<TrackSectorList<'a>>;
187
188/// Parse a track / sector list.
189pub fn parse_track_sector_list(i: &[u8]) -> IResult<&[u8], TrackSectorList<'_>> {
190    let mut track_sector_pairs: Vec<TrackSectorPair> = Vec::new();
191
192    let (i, reserved) = le_u8(i)?;
193    let (i, track_number_of_next_sector) = le_u8(i)?;
194    // TODO: See if there is a more ergonomic way of doing this
195    let track_number_of_next_sector = if track_number_of_next_sector != 0 {
196        Some(track_number_of_next_sector)
197    } else {
198        None
199    };
200
201    let (i, sector_number_of_next_sector) = le_u8(i)?;
202    let sector_number_of_next_sector = if sector_number_of_next_sector != 0 {
203        Some(sector_number_of_next_sector)
204    } else {
205        None
206    };
207
208    let (i, reserved_2) = take(2_usize)(i)?;
209    let (i, sector_offset_in_file) = take(2_usize)(i)?;
210    let (i, reserved_3) = take(5_usize)(i)?;
211
212    let (mut i, mut track_sector_pair) = parse_track_sector_pair(i)?;
213
214    let max_tsps = 121;
215    let mut cnt = 1;
216    while (track_sector_pair.track_number != 0) && (cnt <= max_tsps) {
217        track_sector_pairs.push(track_sector_pair);
218        let (i2, tsp) = parse_track_sector_pair(i)?;
219        track_sector_pair = tsp;
220        i = i2;
221        cnt += 1;
222    }
223
224    Ok((
225        i,
226        TrackSectorList {
227            reserved,
228            track_number_of_next_sector,
229            sector_number_of_next_sector,
230            reserved_2,
231            sector_offset_in_file,
232            reserved_3,
233            track_sector_pairs,
234        },
235    ))
236}
237
238/// Pairs of track and sector numbers used in Track/Sector Lists
239#[derive(Clone, Copy, Debug)]
240pub struct TrackSectorPair {
241    /// The track number
242    pub track_number: u8,
243    /// The sector number
244    pub sector_number: u8,
245}
246
247impl Display for TrackSectorPair {
248    fn fmt(&self, f: &mut Formatter) -> Result {
249        write!(
250            f,
251            "track_number: 0x{:02X}, sector_number: 0x{:02X}",
252            self.track_number, self.sector_number
253        )
254    }
255}
256
257impl<'a> Serializer<'a> for TrackSectorPair {
258    fn as_vec(&'a self) -> std::result::Result<Vec<u8>, crate::error::Error> {
259        let bytes: Vec<u8> = vec![self.track_number, self.sector_number];
260
261        Ok(bytes)
262    }
263}
264
265/// TrackSectorPairs is a list / vector of TrackSectorPair
266pub type TrackSectorPairs = Vec<TrackSectorPair>;
267
268impl<'a> Serializer<'a> for TrackSectorPairs {
269    fn as_vec(&'a self) -> std::result::Result<Vec<u8>, crate::error::Error> {
270        let mut bytes: Vec<u8> = Vec::new();
271
272        for tsp in self {
273            bytes.append(&mut tsp.as_vec().unwrap());
274        }
275
276        Ok(bytes)
277    }
278}
279
280/// Parse a track/sector pair
281pub fn parse_track_sector_pair(i: &[u8]) -> IResult<&[u8], TrackSectorPair> {
282    let (i, track_number) = le_u8(i)?;
283    let (i, sector_number) = le_u8(i)?;
284
285    Ok((
286        i,
287        TrackSectorPair {
288            track_number,
289            sector_number,
290        },
291    ))
292}
293
294/// Custom implmentations for the FileEntry structure
295/// These patterns would be useful for the FAT parser and other
296/// resource-constrained and performance-oriented target codebases,
297/// as long as caching isn't required
298/// A separate data structure could be added to cache the result of these
299/// filename calculations
300impl<'a> FileEntry<'a> {
301    /// Create a new FileEntry with the given data
302    ///
303    /// # Examples
304    ///
305    /// ```
306    /// use image_rider::disk_format::apple::catalog::{FileEntry, FileType};
307    ///
308    /// let fe = FileEntry::new(0x12, 0x0F, FileType::AppleSoftBasic, false, "HELLO", 0x0002);
309    /// assert_eq!(fe.filename().unwrap(), "HELLO");
310    /// ```
311    pub fn new(
312        track_of_first_track_sector_list_sector: u8,
313        sector_of_first_track_sector_list_sector: u8,
314        file_type: FileType,
315        locked: bool,
316        filename: &str,
317        file_length_in_sectors: u16,
318    ) -> FileEntry<'_> {
319        FileEntry {
320            track_of_first_track_sector_list_sector,
321            sector_of_first_track_sector_list_sector,
322            file_type,
323            locked,
324            file_name: filename.as_bytes(),
325            file_length_in_sectors,
326        }
327    }
328
329    /// Return the filename as a String
330    pub fn filename(&self) -> std::result::Result<String, FromUtf8Error> {
331        let filename_vector: Vec<u8> = self
332            .file_name
333            .iter()
334            .map(|c| if *c > 0x80 { *c - 0x80 } else { *c })
335            .collect();
336        let file_name = String::from_utf8(filename_vector)?;
337
338        // Apple DOS disks use spaces as padding at the end
339        // Remove the spaces from the end
340        let file_name = String::from(file_name.trim_end_matches(' '));
341        Ok(file_name)
342    }
343
344    /// Get the data for a file
345    pub fn get_data(
346        &self,
347        tracks: &[Vec<&[u8]>],
348        track_sector_lists: &TrackSectorLists,
349    ) -> std::result::Result<Vec<u8>, crate::error::Error> {
350        // We could build a custom iterator for TrackSectorList, since the
351        // normal meaning of iterating through a TrackSectorList is iterating
352        // through TrackSectorPairs
353        // let data: Vec<u8> = track_sector_lists
354        //     .into_iter()
355        //     .flat_map(|tsl| tsl.track_sector_pairs.clone())
356        //     .flat_map(|tsp| tracks[tsp.track_number as usize][tsp.sector_number as usize])
357        //     .map(|b| *b)
358        //     .collect::<Vec<u8>>();
359        let data: Vec<u8> = track_sector_lists
360            .iter()
361            .flat_map(|tsl| tsl.track_sector_pairs.clone())
362            .flat_map(|tsp| tracks[tsp.track_number as usize][tsp.sector_number as usize])
363            .copied()
364            .collect();
365
366        match self.file_type {
367            FileType::Binary => {
368                if data.len() >= 4 {
369                    let (i, address) = le_u16(data.as_slice())?;
370                    debug!("Binary file address: {}", address);
371                    let (_i, len) = le_u16(i)?;
372                    debug!("Binary file length: {}", len);
373                    // Some additional checking
374                    if (data.len() - 4) >= len.into() {
375                        Ok(data[4..(len + 4) as usize].to_vec())
376                    } else {
377                        Ok(data)
378                    }
379                } else {
380                    Ok(data)
381                }
382            }
383            _ => {
384                let error = crate::error::Error::new(crate::error::ErrorKind::Invalid(
385                    crate::error::InvalidErrorKind::Invalid(format!(
386                        "Unsupported file type for export: {}",
387                        self.file_type
388                    )),
389                ));
390                debug!("{}", error);
391                Err(error)
392            }
393        }
394    }
395
396    /// Build a file from a file entry
397    /// TODO: Get the tracks / sectors down correctly
398    /// E.g. tracks are a vector of sectors
399    pub fn build_file(
400        &self,
401        tracks: &[Vec<&'a [u8]>],
402    ) -> std::result::Result<TrackSectorLists<'a>, crate::error::Error> {
403        let mut track_sector_lists: TrackSectorLists = Vec::new();
404
405        let track = self.track_of_first_track_sector_list_sector;
406        let sector = self.sector_of_first_track_sector_list_sector;
407
408        // There is always at least one track and sector list for a file
409        let (_i, track_sector_list) =
410            parse_track_sector_list(tracks[track as usize][sector as usize]).unwrap();
411        track_sector_lists.push(track_sector_list.clone());
412
413        let mut track = track_sector_list.clone().track_number_of_next_sector;
414        let mut sector = track_sector_list.clone().sector_number_of_next_sector;
415        debug!("track sector list: {}", track_sector_lists.first().unwrap());
416
417        while track.is_some() {
418            debug!(
419                "TSList track {}, sector {}",
420                track.unwrap(),
421                sector.unwrap()
422            );
423            let (_i, track_sector_list) =
424                parse_track_sector_list(tracks[track.unwrap() as usize][sector.unwrap() as usize])
425                    .unwrap();
426            track = track_sector_list.track_number_of_next_sector;
427            sector = track_sector_list.sector_number_of_next_sector;
428            track_sector_lists.push(track_sector_list);
429        }
430
431        Ok(track_sector_lists)
432    }
433}
434
435impl<'a> Serializer<'a> for FileEntry<'a> {
436    fn as_vec(&'a self) -> std::result::Result<Vec<u8>, crate::error::Error> {
437        let mut bytes: Vec<u8> = Vec::new();
438
439        bytes.push(self.track_of_first_track_sector_list_sector);
440        bytes.push(self.sector_of_first_track_sector_list_sector);
441
442        let file_type = if self.locked {
443            self.file_type as u8 + 0x80
444        } else {
445            self.file_type as u8
446        };
447
448        bytes.push(file_type);
449
450        let num_bytes = self.file_name.len();
451        // This may be misusing the ErrorKind::Invalid type
452        if (num_bytes == 0) || (num_bytes > 30) {
453            return Err(crate::error::Error::new(crate::error::ErrorKind::Invalid(
454                crate::error::InvalidErrorKind::Invalid(format!(
455                    "Filename size is invalid: {}",
456                    num_bytes
457                )),
458            )));
459        }
460
461        let mut padding: Vec<u8> = vec![0; 30 - num_bytes];
462
463        padding.fill(0xA0);
464
465        let mut converted_filename: Vec<u8> =
466            self.file_name.to_vec().iter().map(|c| c + 0x80).collect();
467
468        bytes.append(&mut converted_filename);
469        bytes.append(&mut padding);
470        bytes.append(&mut little_endian_word_to_bytes(
471            self.file_length_in_sectors,
472        ));
473
474        Ok(bytes)
475    }
476}
477
478/// A FileEntry with associated file data
479/// Should rename this.
480pub struct FullFile<'a> {
481    /// The associated FileEntry for this file.
482    pub file_entry: FileEntry<'a>,
483    /// The raw data for this file, not including metadata or
484    /// structured information.
485    pub data: Vec<u8>,
486    /// The address this file was at in memory .
487    pub address: u16,
488    /// The length of this file in bytes.
489    pub length: u16,
490}
491
492/// This serializes a File to a block of memory, encoding things like
493/// the address and length for a binary file as header bytes Or
494/// padding with carriage returns or null bytes.
495impl<'a> Serializer<'a> for FullFile<'a> {
496    fn as_vec(&'a self) -> std::result::Result<Vec<u8>, crate::error::Error> {
497        let mut bytes: Vec<u8> = Vec::new();
498        match self.file_entry.file_type {
499            FileType::Binary => {
500                bytes.append(&mut little_endian_word_to_bytes(self.address));
501                bytes.append(&mut little_endian_word_to_bytes(self.length));
502                // TODO: Fix this
503                bytes.append(&mut self.data.clone());
504
505                Ok(bytes)
506            }
507            _ => Err(crate::error::Error::new(
508                crate::error::ErrorKind::Unimplemented(format!(
509                    "Unsupported file tyep for serialization: {}",
510                    self.file_entry.file_type
511                )),
512            )),
513        }
514    }
515}
516
517/// Format a FileEntry for display
518impl Display for FileEntry<'_> {
519    fn fmt(&self, f: &mut Formatter) -> Result {
520        writeln!(
521            f,
522            "{:>3} {:>3} {} {:>3} {:<30}",
523            self.track_of_first_track_sector_list_sector,
524            self.sector_of_first_track_sector_list_sector,
525            self.file_type,
526            self.file_length_in_sectors,
527            self.filename().unwrap_or_else(|_| String::from("")),
528        )
529    }
530}
531
532/// Parse a file entry
533pub fn parse_file_entry(i: &[u8]) -> IResult<&[u8], FileEntry<'_>> {
534    let (i, track_of_first_track_sector_list_sector) = le_u8(i)?;
535    let (i, sector_of_first_track_sector_list_sector) = le_u8(i)?;
536
537    let (i, file_type) = le_u8(i)?;
538
539    // The file type code the disk contains information about the
540    // file type and also whether the file is locked.  If the file is
541    // locked, bit seven is set.
542    let locked = (file_type & 0x80) != 0;
543
544    let file_type = match file_type & 0x7F {
545        0 => FileType::Text,
546        1 => FileType::IntegerBasic,
547        2 => FileType::AppleSoftBasic,
548        4 => FileType::Binary,
549        8 => FileType::SType,
550        10 => FileType::RelocatableObjectModule,
551        20 => FileType::AType,
552        40 => FileType::BType,
553        _ => FileType::Unknown,
554    };
555
556    let (i, filename) = take(30_usize)(i)?;
557    let (i, file_length_in_sectors) = le_u16(i)?;
558
559    Ok((
560        i,
561        FileEntry {
562            track_of_first_track_sector_list_sector,
563            sector_of_first_track_sector_list_sector,
564            file_type,
565            locked,
566            file_name: filename,
567            file_length_in_sectors,
568        },
569    ))
570}
571
572/// Returns a successful result if this is a valid file entry
573/// Otherwise returns an error
574pub fn valid_file_entry(i: &[u8]) -> IResult<&[u8], bool> {
575    let (i, res1) = le_u8(i)?;
576    let (i, res2) = le_u8(i)?;
577
578    if (res1 != 0) && (res2 != 0) {
579        Ok((i, true))
580    } else {
581        Err(Err::Error(nom::error_position!(i, ErrorKind::Fail)))
582    }
583}
584
585/// The disk catalog
586#[derive(Clone)]
587pub struct Catalog<'a> {
588    /// One reserved byte
589    pub reserved: u8,
590    /// The track number of the next catalog sector
591    /// When this is zero, it's the end of the catalog chain
592    pub track_number_of_next_sector: u8,
593    /// The sector number of the next catalog sector
594    pub sector_number_of_next_sector: u8,
595    /// Eight reserved bytes
596    pub reserved_2: &'a [u8],
597
598    /// Up to seven file descriptive entries
599    pub file_entries: Vec<FileEntry<'a>>,
600
601    /// The files in the catalog indexed by filename
602    pub catalog_by_filename: HashMap<String, FileEntry<'a>>,
603}
604
605/// Format a Catalog for display
606impl Display for Catalog<'_> {
607    fn fmt(&self, f: &mut Formatter) -> Result {
608        writeln!(
609            f,
610            "track number of next sector: {}",
611            self.track_number_of_next_sector
612        )?;
613        writeln!(
614            f,
615            "sector number of next sector: {}",
616            self.sector_number_of_next_sector
617        )?;
618        for file_entry in &self.file_entries {
619            write!(f, "{}", file_entry)?;
620        }
621        writeln!(f)
622    }
623}
624
625impl<'a> Serializer<'a> for Catalog<'a> {
626    fn as_vec(&'a self) -> std::result::Result<Vec<u8>, crate::error::Error> {
627        let mut v: Vec<u8> = Vec::new();
628
629        v.push(self.reserved);
630        v.push(self.track_number_of_next_sector);
631        v.push(self.sector_number_of_next_sector);
632
633        v.append(&mut self.reserved_2.to_vec());
634        let mut file_entries: Vec<u8> = self
635            .file_entries
636            .iter()
637            .flat_map(|fe| fe.as_vec())
638            .flatten()
639            .collect();
640
641        let padding_len = (7 - self.file_entries.len()) * 35;
642        let mut padding: Vec<u8> = vec![0; padding_len];
643
644        v.append(&mut file_entries);
645        v.append(&mut padding);
646
647        Ok(v)
648    }
649}
650
651/// Return true if this is a valid allocated undeleted file
652pub fn valid_file(track_of_first_track_sector_list_sector: u8) -> bool {
653    // Unallocated files are set to 0x00 for the location
654    // Deleted files are set to 0xFF for the location
655    (track_of_first_track_sector_list_sector != 0x00)
656        && (track_of_first_track_sector_list_sector != 0xFF)
657}
658
659/// Parse an Apple ][ DOS disk catalog
660pub fn parse_catalog(i: &[u8]) -> IResult<&[u8], Catalog<'_>> {
661    let (i, reserved) = le_u8(i)?;
662    let (i, track_number_of_next_sector) = le_u8(i)?;
663    let (i, sector_number_of_next_sector) = le_u8(i)?;
664    let (i, reserved_2) = take(8_usize)(i)?;
665    // We can also use many_till here to parse out the entries until there is a
666    // file entry with zero for track and sector list entries
667
668    // let (i, file_entries) = many_till(parse_file_entry, valid_file_entry)(i)?;
669
670    let (i, file_entries) = count(parse_file_entry, 7)(i)?;
671
672    let file_entries: Vec<FileEntry> = file_entries
673        .iter()
674        .filter(|fe| valid_file(fe.track_of_first_track_sector_list_sector))
675        .copied()
676        .collect();
677
678    // debug!("file_entries: {:?}", file_entries);
679
680    let mut catalog_by_filename: HashMap<String, FileEntry> = HashMap::new();
681
682    file_entries.iter().for_each(|fe| {
683        catalog_by_filename.insert(fe.filename().unwrap(), *fe);
684    });
685
686    Ok((
687        i,
688        Catalog {
689            reserved,
690            track_number_of_next_sector,
691            sector_number_of_next_sector,
692            reserved_2,
693            file_entries,
694            catalog_by_filename,
695        },
696    ))
697}
698
699/// A FullCatalog combines several Catalog sectors with FileEntries
700/// into a single catalog without the metadata
701#[derive(Clone, Debug)]
702pub struct FullCatalog<'a> {
703    /// Up to seven file descriptive entries
704    pub file_entries: Vec<FileEntry<'a>>,
705
706    /// The files in the catalog indexed by filename
707    pub catalog_by_filename: HashMap<String, FileEntry<'a>>,
708}
709
710/// Format a Catalog for display
711impl Display for FullCatalog<'_> {
712    fn fmt(&self, f: &mut Formatter) -> Result {
713        for file_entry in &self.file_entries {
714            write!(f, "{}", file_entry)?;
715        }
716        writeln!(f)
717    }
718}
719
720/// Parse a series of catalog sectors
721/// This parses all of the catalog sectors and builds a directory of files
722pub fn parse_catalogs<'a>(
723    tracks: &[Vec<&'a [u8]>],
724    catalog_track: u8,
725    catalog_sector: u8,
726) -> std::result::Result<FullCatalog<'a>, crate::error::Error> {
727    let mut file_entries: Vec<FileEntry> = Vec::new();
728    let mut catalog_by_filename: HashMap<String, FileEntry> = HashMap::new();
729
730    let (_i, mut catalog) = parse_catalog(tracks[catalog_track as usize][catalog_sector as usize])?;
731
732    // Show info about the tracks data structure
733    debug!("tracks length: {}", tracks.len());
734    debug!("track one length: {}", tracks[0].len());
735
736    // debug!("Number of files: {}", &catalog.file_entries.len());
737    for file in &catalog.file_entries {
738        file_entries.push(*file);
739        catalog_by_filename.insert(file.filename().unwrap(), *file);
740        // debug!("Filename: {}", file.filename().unwrap());
741    }
742    // debug!("catalog: {}", catalog.clone());
743
744    // The first track and first sector usually contain the DOS boot
745    // code (or a boot stub), so they cannot be used as a catalog
746    // sector.
747    while (catalog.track_number_of_next_sector != 0) && (catalog.sector_number_of_next_sector != 0)
748    {
749        let (_i, c) = parse_catalog(
750            tracks[catalog.track_number_of_next_sector as usize]
751                [catalog.sector_number_of_next_sector as usize],
752        )?;
753
754        debug!("parsed another catalog: {}", c);
755
756        catalog = c;
757        for file in &catalog.file_entries {
758            file_entries.push(*file);
759            catalog_by_filename.insert(file.filename().unwrap(), *file);
760        }
761    }
762
763    Ok(FullCatalog {
764        file_entries,
765        catalog_by_filename,
766    })
767}
768
769impl Catalog<'_> {
770    /// Get the file data for a file in the catalog
771    pub fn get_file(&self, filename: &str) -> Vec<u8> {
772        let _file_entry = self.catalog_by_filename.get(filename).unwrap();
773
774        let data: Vec<u8> = Vec::new();
775        data
776    }
777}
778
779/// Build the files in the catalog
780pub fn build_files<'a>(
781    catalog: FullCatalog<'a>,
782    tracks: &[Vec<&'a [u8]>],
783) -> std::result::Result<Files<'a>, crate::error::Error> {
784    let mut files: Files = HashMap::new();
785
786    for file_entry in &catalog.file_entries {
787        let track_sector_lists = file_entry.build_file(tracks)?;
788        debug!("Building file: {}", file_entry.filename().unwrap());
789        let res = file_entry.get_data(tracks, &track_sector_lists);
790        let data = res.unwrap_or_default();
791
792        files.insert(
793            file_entry.filename().unwrap(),
794            File {
795                track_sector_lists,
796                data,
797            },
798        );
799    }
800    Ok(files)
801}
802
803#[cfg(test)]
804mod tests {
805    use super::{
806        build_files, parse_catalog, parse_catalogs, parse_file_entry, Catalog, FileEntry, FileType,
807        TrackSectorList, TrackSectorPair, TrackSectorPairs,
808    };
809    use crate::serialize::{little_endian_word_to_bytes, Serializer};
810    use nom::AsBytes;
811    use pretty_assertions::assert_eq;
812    use std::collections::HashMap;
813
814    /// Returns a 35-byte file entry with a given filename
815    fn file_entry_as_bytes(
816        file_entry: &FileEntry,
817    ) -> std::result::Result<[u8; 35], crate::error::Error> {
818        Ok(file_entry.as_vec()?.as_bytes().try_into().unwrap())
819    }
820
821    /// Test that parsing a file entry works
822    #[test]
823    fn parse_file_entry_works() {
824        let data: [u8; 35] = [
825            0x12, 0x0F, 0x02, 0xC8, 0xC5, 0xCC, 0xCC, 0xCF, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
826            0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
827            0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0x02, 0x00,
828        ];
829
830        let result = parse_file_entry(&data);
831
832        match result {
833            Ok(file_entry) => {
834                assert_eq!(file_entry.1.track_of_first_track_sector_list_sector, 18);
835                assert_eq!(file_entry.1.sector_of_first_track_sector_list_sector, 15);
836                match file_entry.1.file_type {
837                    FileType::AppleSoftBasic => {
838                        assert_eq!(true, true);
839                    }
840                    _ => {
841                        panic!("Invalid file type parsed");
842                    }
843                }
844                assert!(!file_entry.1.locked);
845                assert_eq!(
846                    file_entry.1.file_name,
847                    [
848                        0xC8, 0xC5, 0xCC, 0xCC, 0xCF, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
849                        0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
850                        0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
851                    ]
852                );
853            }
854            Err(e) => {
855                panic!("Error parsing: {}", e);
856            }
857        }
858    }
859
860    /// Test that parsing a locked file entry works
861    #[test]
862    fn parse_file_entry_locked_works() {
863        let data: [u8; 35] = [
864            0x12, 0x0F, 0x82, 0xC8, 0xC5, 0xCC, 0xCC, 0xCF, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
865            0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
866            0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0x02, 0x00,
867        ];
868
869        let result = parse_file_entry(&data);
870
871        match result {
872            Ok(file_entry) => {
873                assert_eq!(file_entry.1.track_of_first_track_sector_list_sector, 18);
874                assert_eq!(file_entry.1.sector_of_first_track_sector_list_sector, 15);
875                match file_entry.1.file_type {
876                    FileType::AppleSoftBasic => {
877                        assert_eq!(true, true);
878                    }
879                    _ => {
880                        panic!("Invalid file type parsed");
881                    }
882                }
883                assert!(file_entry.1.locked);
884                assert_eq!(
885                    file_entry.1.file_name,
886                    [
887                        0xC8, 0xC5, 0xCC, 0xCC, 0xCF, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
888                        0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
889                        0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
890                    ]
891                );
892            }
893            Err(e) => {
894                panic!("Error parsing: {}", e);
895            }
896        }
897    }
898
899    /// Test that serializing a file entry works
900    #[test]
901    fn serialize_file_entry_works() {
902        let expected_data: [u8; 35] = [
903            0x12, 0x0F, 0x02, 0xC8, 0xC5, 0xCC, 0xCC, 0xCF, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
904            0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
905            0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0x02, 0x00,
906        ];
907
908        let data = file_entry_as_bytes(&FileEntry::new(
909            0x12,
910            0x0F,
911            FileType::AppleSoftBasic,
912            false,
913            "HELLO",
914            0x0002,
915        ));
916
917        assert_eq!(data.unwrap(), expected_data);
918    }
919
920    /// Test that serializing a file entry works
921    #[test]
922    fn serialize_locked_file_entry_works() {
923        let expected_data: [u8; 35] = [
924            0x12, 0x0F, 0x82, 0xC8, 0xC5, 0xCC, 0xCC, 0xCF, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
925            0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
926            0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0x02, 0x00,
927        ];
928
929        let data = file_entry_as_bytes(&FileEntry::new(
930            0x12,
931            0x0F,
932            FileType::AppleSoftBasic,
933            true,
934            "HELLO",
935            0x0002,
936        ));
937
938        assert_eq!(data.unwrap(), expected_data);
939    }
940
941    /// Test that serializing a file entry with a zero length filename
942    /// works.
943    /// Decide whether this should be a type constraint
944    #[test]
945    fn serialize_file_name_len_0_file_entry_fails() {
946        let file_entry = FileEntry::new(0x12, 0x0F, FileType::AppleSoftBasic, false, "", 0x0002);
947
948        let file_entry_as_vec = file_entry.as_vec();
949
950        match file_entry_as_vec {
951            Ok(_) => panic!("Shouldn't be a valid FileEntry"),
952            Err(e) => assert_eq!(
953                e.to_string(),
954                "Image is invalid: Filename size is invalid: 0"
955            ),
956        }
957    }
958
959    /// Test that serializing a file entry with a one length filename
960    /// works
961    #[test]
962    fn serialize_file_name_len_1_file_entry_works() {
963        let expected_data: [u8; 35] = [
964            0x12, 0x0F, 0x02, 0xC8, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
965            0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
966            0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0x02, 0x00,
967        ];
968
969        let data = file_entry_as_bytes(&FileEntry::new(
970            0x12,
971            0x0F,
972            FileType::AppleSoftBasic,
973            false,
974            "H",
975            0x0002,
976        ));
977
978        assert_eq!(data.unwrap(), expected_data);
979    }
980
981    /// Test that serializing a file entry with a 30 length filename
982    /// works
983    #[test]
984    fn serialize_file_name_len_30_file_entry_works() {
985        let expected_data: [u8; 35] = [
986            0x12, 0x0F, 0x02, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xB0,
987            0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4,
988            0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0x02, 0x00,
989        ];
990
991        let data = file_entry_as_bytes(&FileEntry::new(
992            0x12,
993            0x0F,
994            FileType::AppleSoftBasic,
995            false,
996            "012345678901234567890123456789",
997            0x0002,
998        ));
999
1000        assert_eq!(data.unwrap(), expected_data);
1001    }
1002
1003    /// Test that serializing a file entry with a 30 length filename
1004    /// works
1005    #[test]
1006    fn serialize_file_name_len_31_file_entry_fails() {
1007        let file_entry = FileEntry::new(
1008            0x12,
1009            0x0F,
1010            FileType::AppleSoftBasic,
1011            false,
1012            "0123456789012345678901234567890",
1013            0x0002,
1014        );
1015
1016        let file_entry_as_vec = file_entry.as_vec();
1017
1018        match file_entry_as_vec {
1019            Ok(_) => panic!("Shouldn't be a valid FileEntry"),
1020            Err(e) => assert_eq!(
1021                e.to_string(),
1022                "Image is invalid: Filename size is invalid: 31"
1023            ),
1024        }
1025    }
1026
1027    /// Test that converting a filename works
1028    #[test]
1029    fn file_entry_filename_works() {
1030        let data: [u8; 35] = [
1031            0x12, 0x0F, 0x02, 0xC8, 0xC5, 0xCC, 0xCC, 0xCF, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
1032            0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
1033            0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0x02, 0x00,
1034        ];
1035
1036        let result = parse_file_entry(&data);
1037
1038        match result {
1039            Ok(file_entry) => match file_entry.1.filename() {
1040                Ok(filename) => {
1041                    assert_eq!(filename, "HELLO");
1042                }
1043                Err(e) => {
1044                    panic!("Invalid filename: {}", e);
1045                }
1046            },
1047            Err(e) => {
1048                panic!("Error parsing: {}", e);
1049            }
1050        }
1051    }
1052
1053    /// Test that serializing a TrackSectorPair works.
1054    #[test]
1055    fn serialize_track_sector_pair_works() {
1056        let tsp = TrackSectorPair {
1057            track_number: 0x12,
1058            sector_number: 0x34,
1059        };
1060
1061        let data = tsp.as_vec().unwrap();
1062
1063        assert_eq!(data.len(), 2);
1064        assert_eq!(data[0], 0x12);
1065        assert_eq!(data[1], 0x34);
1066    }
1067
1068    /// Test that serializing a TrackSectorList with zero
1069    /// TrackSectorPair works
1070    #[test]
1071    fn serialize_track_sector_list_with_zero_track_sector_pair_works() {
1072        let tsl = TrackSectorList {
1073            reserved: 0x01,
1074            track_number_of_next_sector: None,
1075            sector_number_of_next_sector: None,
1076            reserved_2: &[0x02, 0x03],
1077            sector_offset_in_file: &[0x04, 0x05],
1078            reserved_3: &[0x06, 0x07, 0x08, 0x09, 0x10],
1079            track_sector_pairs: Vec::new(),
1080        };
1081
1082        let data = tsl.as_vec().unwrap();
1083
1084        assert_eq!(data.len(), 12);
1085        assert_eq!(data[0], 0x01);
1086        assert_eq!(data[1], 0x00);
1087        assert_eq!(data[2], 0x00);
1088        assert_eq!(data[3], 0x02);
1089        assert_eq!(data[4], 0x03);
1090        assert_eq!(data[5], 0x04);
1091        assert_eq!(data[6], 0x05);
1092        assert_eq!(data[7], 0x06);
1093        assert_eq!(data[8], 0x07);
1094        assert_eq!(data[9], 0x08);
1095        assert_eq!(data[10], 0x09);
1096        assert_eq!(data[11], 0x10);
1097    }
1098
1099    /// Test that serializing a TrackSectorList with one
1100    /// TrackSectorPair works
1101    #[test]
1102    fn serialize_track_sector_list_with_one_track_sector_pair_works() {
1103        let tsp = TrackSectorPair {
1104            track_number: 0x12,
1105            sector_number: 0x34,
1106        };
1107        let tsps = Vec::from([tsp]);
1108
1109        let tsl = TrackSectorList {
1110            reserved: 0x01,
1111            track_number_of_next_sector: None,
1112            sector_number_of_next_sector: None,
1113            reserved_2: &[0x02, 0x03],
1114            sector_offset_in_file: &[0x04, 0x05],
1115            reserved_3: &[0x06, 0x07, 0x08, 0x09, 0x10],
1116            track_sector_pairs: tsps,
1117        };
1118
1119        let data = tsl.as_vec().unwrap();
1120
1121        assert_eq!(data.len(), 14);
1122        assert_eq!(data[0], 0x01);
1123        assert_eq!(data[1], 0x00);
1124        assert_eq!(data[2], 0x00);
1125        assert_eq!(data[3], 0x02);
1126        assert_eq!(data[4], 0x03);
1127        assert_eq!(data[5], 0x04);
1128        assert_eq!(data[6], 0x05);
1129        assert_eq!(data[7], 0x06);
1130        assert_eq!(data[8], 0x07);
1131        assert_eq!(data[9], 0x08);
1132        assert_eq!(data[10], 0x09);
1133        assert_eq!(data[11], 0x10);
1134        assert_eq!(data[12], 0x12);
1135        assert_eq!(data[13], 0x34);
1136    }
1137
1138    /// Test that serializing a TrackSectorList with two
1139    /// TrackSectorPair works
1140    #[test]
1141    fn serialize_track_sector_list_with_two_track_sector_pair_works() {
1142        let tsp1 = TrackSectorPair {
1143            track_number: 0x12,
1144            sector_number: 0x34,
1145        };
1146        let tsp2 = TrackSectorPair {
1147            track_number: 0x56,
1148            sector_number: 0x78,
1149        };
1150        let tsps = Vec::from([tsp1, tsp2]);
1151
1152        let tsl = TrackSectorList {
1153            reserved: 0x01,
1154            track_number_of_next_sector: None,
1155            sector_number_of_next_sector: None,
1156            reserved_2: &[0x02, 0x03],
1157            sector_offset_in_file: &[0x04, 0x05],
1158            reserved_3: &[0x06, 0x07, 0x08, 0x09, 0x10],
1159            track_sector_pairs: tsps,
1160        };
1161
1162        let data = tsl.as_vec().unwrap();
1163
1164        assert_eq!(data.len(), 16);
1165        assert_eq!(data[0], 0x01);
1166        assert_eq!(data[1], 0x00);
1167        assert_eq!(data[2], 0x00);
1168        assert_eq!(data[3], 0x02);
1169        assert_eq!(data[4], 0x03);
1170        assert_eq!(data[5], 0x04);
1171        assert_eq!(data[6], 0x05);
1172        assert_eq!(data[7], 0x06);
1173        assert_eq!(data[8], 0x07);
1174        assert_eq!(data[9], 0x08);
1175        assert_eq!(data[10], 0x09);
1176        assert_eq!(data[11], 0x10);
1177        assert_eq!(data[12], 0x12);
1178        assert_eq!(data[13], 0x34);
1179        assert_eq!(data[14], 0x56);
1180        assert_eq!(data[15], 0x78);
1181    }
1182
1183    /// Test that parsing a catalog works
1184    #[test]
1185    fn parse_catalog_one_file_works() {
1186        // catalog header with a single file
1187        let data_header: [u8; 46] = [
1188            0x00, 0x11, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x0F, 0x02,
1189            0xC8, 0xC5, 0xCC, 0xCC, 0xCF, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
1190            0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
1191            0xA0, 0xA0, 0x02, 0x00,
1192        ];
1193        let data_footer: [u8; 210] = [0; 210];
1194        let mut data: Vec<u8> = Vec::new();
1195
1196        data.extend(data_header);
1197        data.extend(data_footer);
1198
1199        let result = parse_catalog(&data);
1200
1201        match result {
1202            Ok(catalog) => {
1203                assert_eq!(catalog.1.file_entries.len(), 1);
1204                let file_entry = catalog.1.file_entries.first().unwrap_or_else(|| {
1205                    panic!("Error getting file entry");
1206                });
1207                let filename = file_entry.filename().unwrap_or_else(|e| {
1208                    panic!("Error getting file name: {}", e);
1209                });
1210                assert_eq!(filename, "HELLO");
1211            }
1212            Err(e) => {
1213                panic!("Error parsing: {}", e);
1214            }
1215        }
1216    }
1217
1218    /// Test that parsing a catalog with two files works
1219    #[test]
1220    fn parse_catalog_two_files_works() {
1221        // catalog header with a single file
1222        let data_header: [u8; 81] = [
1223            0x00, 0x11, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x0F, 0x02,
1224            0xC8, 0xC5, 0xCC, 0xCC, 0xCF, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
1225            0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
1226            0xA0, 0xA0, 0x02, 0x00, 0x12, 0x0F, 0x02, 0xC8, 0xC5, 0xCC, 0xD0, 0xA0, 0xA0, 0xA0,
1227            0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0,
1228            0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0xA0, 0x02, 0x00,
1229        ];
1230        let data_footer: [u8; 210] = [0; 210];
1231        let mut data: Vec<u8> = Vec::new();
1232
1233        data.extend(data_header);
1234        data.extend(data_footer);
1235
1236        let result = parse_catalog(&data);
1237
1238        match result {
1239            Ok(catalog) => {
1240                assert_eq!(catalog.1.file_entries.len(), 2);
1241
1242                let file_entry = catalog.1.file_entries.first().unwrap_or_else(|| {
1243                    panic!("Error getting file entry");
1244                });
1245                let filename = file_entry.filename().unwrap_or_else(|e| {
1246                    panic!("Error getting file name: {}", e);
1247                });
1248                assert_eq!(filename, "HELLO");
1249                let file_entry = catalog
1250                    .1
1251                    .file_entries
1252                    .get(1)
1253                    .ok_or_else(|| {
1254                        panic!("Error getting file entry");
1255                    })
1256                    .unwrap_or_else(|_e| {
1257                        panic!("Error getting file entry");
1258                    });
1259                let filename = file_entry.filename().unwrap_or_else(|e| {
1260                    panic!("Error getting file name: {}", e);
1261                });
1262                assert_eq!(filename, "HELP");
1263            }
1264            Err(e) => {
1265                panic!("Error parsing: {}", e);
1266            }
1267        }
1268    }
1269
1270    /// Test that parsing a single-sector catalog with the new test
1271    /// helpers works.
1272    /// This catalog just has one file
1273    /// Test that parsing a catalog that spans two sectors works.
1274    #[test]
1275    fn parse_single_sector_catalog_works() {
1276        let file_entries_1 = [FileEntry::new(
1277            0x12,
1278            0x0F,
1279            FileType::AppleSoftBasic,
1280            false,
1281            "A",
1282            0x0002,
1283        )];
1284
1285        let mut catalog_by_filename_1: HashMap<String, FileEntry> = HashMap::new();
1286        file_entries_1.iter().for_each(|fe| {
1287            catalog_by_filename_1.insert(fe.filename().unwrap(), *fe);
1288        });
1289
1290        let catalog_1 = Catalog {
1291            reserved: 0x00,
1292            track_number_of_next_sector: 0x00,
1293            sector_number_of_next_sector: 0x00,
1294            reserved_2: &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
1295            file_entries: file_entries_1.to_vec(),
1296            catalog_by_filename: catalog_by_filename_1,
1297        };
1298
1299        let catalog_1_bytes = catalog_1.as_vec().unwrap();
1300
1301        let mut tracks: Vec<Vec<&[u8]>> = Vec::new();
1302
1303        let mut disk_data: [[[u8; 256]; 16]; 35] = [[[0; 256]; 16]; 35];
1304
1305        for (i, byte) in catalog_1_bytes.iter().enumerate() {
1306            disk_data[17][2][i] = *byte;
1307        }
1308
1309        for track in &disk_data {
1310            let mut track_vec: Vec<&[u8]> = Vec::new();
1311            for sector in track {
1312                track_vec.push(sector);
1313            }
1314            tracks.push(track_vec);
1315        }
1316
1317        let catalog = parse_catalogs(&tracks, 17, 2).expect("Should be a valid FullCatalog");
1318        assert_eq!(catalog.file_entries.len(), 1);
1319        assert_eq!(
1320            catalog
1321                .file_entries
1322                .first()
1323                .expect("Should have at least one file")
1324                .filename()
1325                .expect("Should be a valid filename"),
1326            "A"
1327        );
1328    }
1329
1330    /// Test that parsing a catalog that spans two sectors works.
1331    #[test]
1332    fn parse_multi_sector_catalog_works() {
1333        let file_entries_1 = [
1334            FileEntry::new(0x12, 0x0F, FileType::AppleSoftBasic, false, "A", 0x0002),
1335            FileEntry::new(0x13, 0x0F, FileType::AppleSoftBasic, false, "B", 0x0002),
1336            FileEntry::new(0x14, 0x0F, FileType::AppleSoftBasic, false, "C", 0x0002),
1337            FileEntry::new(0x15, 0x0F, FileType::AppleSoftBasic, false, "D", 0x0002),
1338            FileEntry::new(0x16, 0x0F, FileType::AppleSoftBasic, false, "E", 0x0002),
1339            FileEntry::new(0x17, 0x0F, FileType::AppleSoftBasic, false, "F", 0x0002),
1340            FileEntry::new(0x18, 0x0F, FileType::AppleSoftBasic, false, "G", 0x0002),
1341        ];
1342        let file_entries_2 = [
1343            FileEntry::new(0x19, 0x0F, FileType::AppleSoftBasic, false, "H", 0x0002),
1344            FileEntry::new(0x1A, 0x0F, FileType::AppleSoftBasic, false, "I", 0x0002),
1345            FileEntry::new(0x1B, 0x0F, FileType::AppleSoftBasic, false, "J", 0x0002),
1346        ];
1347
1348        let mut catalog_by_filename_1: HashMap<String, FileEntry> = HashMap::new();
1349        file_entries_1.iter().for_each(|fe| {
1350            catalog_by_filename_1.insert(fe.filename().unwrap(), *fe);
1351        });
1352
1353        let mut catalog_by_filename_2: HashMap<String, FileEntry> = HashMap::new();
1354        file_entries_2.iter().for_each(|fe| {
1355            catalog_by_filename_2.insert(fe.filename().unwrap(), *fe);
1356        });
1357
1358        let catalog_1 = Catalog {
1359            reserved: 0x00,
1360            track_number_of_next_sector: 0x11,
1361            sector_number_of_next_sector: 0x01,
1362            reserved_2: &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
1363            file_entries: file_entries_1.to_vec(),
1364            catalog_by_filename: catalog_by_filename_1,
1365        };
1366        let catalog_2 = Catalog {
1367            reserved: 0x00,
1368            track_number_of_next_sector: 0x00,
1369            sector_number_of_next_sector: 0x00,
1370            reserved_2: &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
1371            file_entries: file_entries_2.to_vec(),
1372            catalog_by_filename: catalog_by_filename_2,
1373        };
1374
1375        let catalog_1_bytes = catalog_1.as_vec().unwrap();
1376        let catalog_2_bytes = catalog_2.as_vec().unwrap();
1377
1378        let mut tracks: Vec<Vec<&[u8]>> = Vec::new();
1379
1380        let mut disk_data: [[[u8; 256]; 16]; 35] = [[[0; 256]; 16]; 35];
1381
1382        for (i, byte) in catalog_1_bytes.iter().enumerate() {
1383            disk_data[17][2][i] = *byte;
1384        }
1385        for (i, byte) in catalog_2_bytes.iter().enumerate() {
1386            disk_data[17][1][i] = *byte;
1387        }
1388
1389        for track in &disk_data {
1390            let mut track_vec: Vec<&[u8]> = Vec::new();
1391            for sector in track {
1392                track_vec.push(sector);
1393            }
1394            tracks.push(track_vec);
1395        }
1396
1397        let catalog = parse_catalogs(&tracks, 17, 2).expect("Should be a valid FullCatalog");
1398        assert_eq!(catalog.file_entries.len(), 10);
1399        assert_eq!(
1400            catalog
1401                .file_entries
1402                .first()
1403                .expect("Should have at least one file")
1404                .filename()
1405                .expect("Should be a valid filename"),
1406            "A"
1407        );
1408    }
1409
1410    /// Build a test binary file with the following content:
1411    /// Starts with the ASCII string START
1412    /// ends with the ASCII string END
1413    /// Filled with repeating data 0x00-0xFF, e.g.:
1414    /// build_test_file(10) ->
1415    /// 53 54 41 52 54 00 01 02 45 4e 44  |START...END|
1416    ///
1417    /// The length of the returned data is eight more bytes than the
1418    /// requested length, this is because it includes the memory
1419    /// address info and the file size in the header.
1420    fn build_binary_test_file(size: u16) -> Vec<u8> {
1421        let mut data: Vec<u8> = Vec::new();
1422
1423        // The address in memory the file was located at
1424        data.extend([0x00, 0x10]);
1425        // The file size in bytes
1426        data.extend(little_endian_word_to_bytes(size));
1427
1428        match size {
1429            0 => {}
1430            1 => {
1431                data.push(0x53);
1432            }
1433            2 => {
1434                data.extend([0x53, 0x44]);
1435            }
1436            3 => {
1437                data.extend([0x53, 0x54, 0x44]);
1438            }
1439            4 => {
1440                data.extend([0x53, 0x54, 0x4e, 0x44]);
1441            }
1442            5 => {
1443                data.extend([0x53, 0x54, 0x41, 0x4e, 0x44]);
1444            }
1445            6 => {
1446                data.extend([0x53, 0x54, 0x41, 0x45, 0x4e, 0x44]);
1447            }
1448            7 => {
1449                data.extend([0x53, 0x54, 0x41, 0x52, 0x45, 0x4e, 0x44]);
1450            }
1451            8 => {
1452                data.extend([0x53, 0x54, 0x41, 0x52, 0x54, 0x45, 0x4e, 0x44]);
1453            }
1454            _ => {
1455                data.extend([0x53, 0x54, 0x41, 0x52, 0x54]);
1456                for i in 0..size - 8 {
1457                    data.push((i % 0x100).try_into().unwrap());
1458                }
1459                data.extend([0x45, 0x4e, 0x44]);
1460            }
1461        }
1462
1463        data
1464    }
1465
1466    /// Test that building a file works
1467    /// Build a file that fits in less than a single sector
1468    /// This is a fairly complicated test function, it should be broken down into multiple
1469    /// functions.
1470    /// First, build a test file, then build a catalog.  Then insert the file into the
1471    /// image and build the disk image.
1472    #[test]
1473    fn build_single_sector_binary_file_works() {
1474        let file_entry = FileEntry::new(0x0A, 0x0D, FileType::Binary, false, "BLAH", 0x0001);
1475        let file_entries_1 = [file_entry];
1476
1477        let mut catalog_by_filename_1: HashMap<String, FileEntry> = HashMap::new();
1478        file_entries_1.iter().for_each(|fe| {
1479            catalog_by_filename_1.insert(fe.filename().unwrap(), *fe);
1480        });
1481
1482        let catalog_1 = Catalog {
1483            reserved: 0x00,
1484            track_number_of_next_sector: 0x00,
1485            sector_number_of_next_sector: 0x00,
1486            reserved_2: &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
1487            file_entries: file_entries_1.to_vec(),
1488            catalog_by_filename: catalog_by_filename_1,
1489        };
1490
1491        let catalog_1_bytes = catalog_1.as_vec().unwrap();
1492
1493        let mut tracks: Vec<Vec<&[u8]>> = Vec::new();
1494
1495        let mut disk_data: [[[u8; 256]; 16]; 35] = [[[0; 256]; 16]; 35];
1496
1497        for (i, byte) in catalog_1_bytes.iter().enumerate() {
1498            disk_data[17][2][i] = *byte;
1499        }
1500
1501        // Using a sector size of 256
1502        let data = build_binary_test_file(200);
1503
1504        for (i, byte) in data.iter().enumerate() {
1505            disk_data[0x11][0x0B][i] = *byte;
1506        }
1507
1508        // Build the TrackSectorList for the file
1509        let tsp = TrackSectorPair {
1510            track_number: 0x11,
1511            sector_number: 0x0B,
1512        };
1513        let tsps: TrackSectorPairs = vec![tsp];
1514
1515        let tsl = TrackSectorList {
1516            reserved: 0,
1517            track_number_of_next_sector: None,
1518            sector_number_of_next_sector: None,
1519            reserved_2: &[0, 0],
1520            sector_offset_in_file: &[0, 0],
1521            reserved_3: &[0, 0, 0, 0, 0],
1522            track_sector_pairs: tsps,
1523        };
1524
1525        for (i, byte) in tsl.as_vec().unwrap().iter().enumerate() {
1526            disk_data[0x0A][0x0D][i] = *byte;
1527        }
1528
1529        for track in &disk_data {
1530            let mut track_vec: Vec<&[u8]> = Vec::new();
1531            for sector in track {
1532                track_vec.push(sector);
1533            }
1534            tracks.push(track_vec);
1535        }
1536
1537        // TODO: Test parsing the catalog and retrieving the file
1538        let catalog = parse_catalogs(&tracks, 17, 2).expect("Should be a valid FullCatalog");
1539        assert_eq!(catalog.file_entries.len(), 1);
1540        assert_eq!(
1541            catalog
1542                .file_entries
1543                .first()
1544                .expect("Should have at least one file")
1545                .filename()
1546                .expect("Should be a valid filename"),
1547            "BLAH"
1548        );
1549
1550        let files = build_files(catalog.clone(), &tracks).unwrap();
1551        assert!(files.contains_key("BLAH"));
1552        assert!(!files.contains_key("BLARGH"));
1553
1554        let file = files.get("BLAH").unwrap();
1555
1556        assert_eq!(file.data.len(), 200);
1557        assert_eq!(&file.data[0..5], "START".as_bytes());
1558        for i in 0..192 {
1559            assert_eq!(file.data[(i as usize) + 5_usize], i);
1560        }
1561        assert_eq!(&file.data[197..200], "END".as_bytes());
1562    }
1563
1564    /// Test that building a file works
1565    /// Build a file that spans two sectors
1566    /// This is a fairly complicated test function, it should be broken down into multiple
1567    /// functions.
1568    /// First, build a test file, then build a catalog.  Then insert the file into the
1569    /// image and build the disk image.
1570    #[test]
1571    fn build_two_sector_binary_file_works() {
1572        let file_entry = FileEntry::new(0x0A, 0x0D, FileType::Binary, false, "BLAH", 0x0002);
1573        let file_entries_1 = [file_entry];
1574
1575        let mut catalog_by_filename_1: HashMap<String, FileEntry> = HashMap::new();
1576        file_entries_1.iter().for_each(|fe| {
1577            catalog_by_filename_1.insert(fe.filename().unwrap(), *fe);
1578        });
1579
1580        let catalog_1 = Catalog {
1581            reserved: 0x00,
1582            track_number_of_next_sector: 0x00,
1583            sector_number_of_next_sector: 0x00,
1584            reserved_2: &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
1585            file_entries: file_entries_1.to_vec(),
1586            catalog_by_filename: catalog_by_filename_1,
1587        };
1588
1589        let catalog_1_bytes = catalog_1.as_vec().unwrap();
1590
1591        let mut tracks: Vec<Vec<&[u8]>> = Vec::new();
1592
1593        let mut disk_data: [[[u8; 256]; 16]; 35] = [[[0; 256]; 16]; 35];
1594
1595        for (i, byte) in catalog_1_bytes.iter().enumerate() {
1596            disk_data[17][2][i] = *byte;
1597        }
1598
1599        // Using a sector size of 256
1600        // Build a binary file of 400 bytes
1601        let data = build_binary_test_file(400);
1602
1603        for (i, byte) in data[0..=255].iter().enumerate() {
1604            disk_data[0x11][0x0B][i] = *byte;
1605        }
1606
1607        for (i, byte) in data[256..].iter().enumerate() {
1608            disk_data[0x11][0x0C][i] = *byte;
1609        }
1610
1611        // Build the TrackSectorList for the file
1612        let tsp1 = TrackSectorPair {
1613            track_number: 0x11,
1614            sector_number: 0x0B,
1615        };
1616        let tsp2 = TrackSectorPair {
1617            track_number: 0x11,
1618            sector_number: 0x0C,
1619        };
1620        let tsps: TrackSectorPairs = vec![tsp1, tsp2];
1621
1622        let tsl = TrackSectorList {
1623            reserved: 0,
1624            track_number_of_next_sector: None,
1625            sector_number_of_next_sector: None,
1626            reserved_2: &[0, 0],
1627            sector_offset_in_file: &[0, 0],
1628            reserved_3: &[0, 0, 0, 0, 0],
1629            track_sector_pairs: tsps,
1630        };
1631
1632        for (i, byte) in tsl.as_vec().unwrap().iter().enumerate() {
1633            disk_data[0x0A][0x0D][i] = *byte;
1634        }
1635
1636        for track in &disk_data {
1637            let mut track_vec: Vec<&[u8]> = Vec::new();
1638            for sector in track {
1639                track_vec.push(sector);
1640            }
1641            tracks.push(track_vec);
1642        }
1643
1644        // TODO: Test parsing the catalog and retrieving the file
1645        let catalog = parse_catalogs(&tracks, 17, 2).expect("Should be a valid FullCatalog");
1646        assert_eq!(catalog.file_entries.len(), 1);
1647        assert_eq!(
1648            catalog
1649                .file_entries
1650                .first()
1651                .expect("Should have at least one file")
1652                .filename()
1653                .expect("Should be a valid filename"),
1654            "BLAH"
1655        );
1656
1657        let files = build_files(catalog.clone(), &tracks).unwrap();
1658        assert!(files.contains_key("BLAH"));
1659        assert!(!files.contains_key("BLARGH"));
1660
1661        let file = files.get("BLAH").unwrap();
1662
1663        assert_eq!(file.data.len(), 400);
1664        assert_eq!(&file.data[0..5], "START".as_bytes());
1665        let expected_data: [u8; 392] = (0_u16..392_u16)
1666            .map(|i| (i % 0x100) as u8)
1667            .collect::<Vec<u8>>()
1668            .try_into()
1669            .unwrap();
1670
1671        assert_eq!(file.data[5..397], expected_data);
1672        assert_eq!(&file.data[397..400], "END".as_bytes());
1673    }
1674}