Skip to main content

image_rider/disk_format/apple/
disk.rs

1//! Disk-level functions and data structures for Apple disks.
2use log::{debug, error, info};
3
4use std::{
5    cmp::min,
6    fs::{self, File},
7    io::Write,
8    path::{Path, PathBuf},
9};
10
11use nom::bytes::complete::take;
12use nom::multi::count;
13use nom::number::complete::{le_i8, le_u16, le_u8};
14use nom::{Err, IResult};
15
16use std::fmt::{Display, Formatter, Result};
17
18use crate::config::Config;
19use crate::disk_format::apple::catalog::{build_files, parse_catalogs, Files, FullCatalog};
20use crate::disk_format::apple::nibble::{parse_nib_disk, recognize_prologue};
21use crate::disk_format::image::{
22    DiskImage, DiskImageGuess, DiskImageGuesser, DiskImageOps, DiskImageParser, DiskImageSaver,
23};
24use crate::disk_format::sanity_check::SanityCheck;
25use crate::error::{Error, ErrorKind, InvalidErrorKind};
26
27use super::nibble::NibbleDisk;
28
29/// The different types of endoding wrappers for the disks
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31pub enum Encoding {
32    /// No special encoding, a raw disk image
33    Plain,
34    /// Nibble encoding for a disk
35    Nibble,
36}
37
38/// Format an Encoding for display
39impl Display for Encoding {
40    fn fmt(&self, f: &mut Formatter) -> Result {
41        write!(f, "{:?}", self)
42    }
43}
44
45/// The different types of Apple disks
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub enum Format {
48    /// Unknown disk format.
49    /// We may not have enough information at the current stage to know the format
50    /// This is a simple data type so it should be fast to update
51    ///
52    /// There's a design decision here to use an Unknown enum variant
53    /// as opposed to an Option with None.  I don't know if I made the
54    /// right choice.
55    Unknown(u64),
56    /// Apple DOS (3.2)
57    DOS32(u64),
58    /// Apple DOS (3.3)
59    DOS33(u64),
60    /// Apple ProDOS
61    ProDOS(u64),
62}
63
64/// Format a Format for display
65impl Display for Format {
66    fn fmt(&self, f: &mut Formatter) -> Result {
67        write!(f, "{:?}", self)
68    }
69}
70
71/// The Volume Table of Contents (VTOC)
72/// The VTOC contains
73pub struct VolumeTableOfContents<'a> {
74    /// Reserved
75    pub reserved: u8,
76
77    /// Track number of first catalog sector
78    pub track_number_of_first_catalog_sector: u8,
79    /// Sector number of first catalog sector
80    pub sector_number_of_first_catalog_sector: u8,
81
82    /// Release number of DOS used to initialize disk
83    pub release_number_of_dos: u8,
84    /// Reserved
85    /// 2 bytes
86    pub reserved2: &'a [u8],
87    /// Diskette volume number (1-254)
88    pub diskette_volume_number: u8,
89    /// Reserved
90    /// 0x20 bytes
91    pub reserved3: &'a [u8],
92
93    /// Maximum number of track/sector pairs which will fit in
94    /// one file track/sector list sector (122 for 256 byte sectors)
95    pub maximum_number_of_track_sector_pairs: u8,
96
97    /// Reserved
98    /// 8 bytes
99    pub reserved4: &'a [u8],
100
101    /// Last track where sectors were allocated
102    pub last_track_where_sectors_were_allocated: u8,
103    /// Direction of track allocation, +1 or -1
104    pub direction_of_track_allocation: i8,
105
106    /// Reserved
107    /// 2 bytes
108    pub reserved5: &'a [u8],
109
110    /// Number of tracks per diskette (normally 35)
111    pub number_of_tracks_per_diskette: u8,
112    /// Number of sectors per track (13 or 16)
113    pub number_of_sectors_per_track: u8,
114    /// Number of bytes per sector (little endian format)
115    pub number_of_bytes_per_sector: u16,
116
117    /// bytes 0x38 - 0xFF
118    /// Each bit map of free sectors for a track is four bytes long
119    /// There is one for each track, usually 35 in DOS 3.3 disks
120    pub bit_map_of_free_sectors: Vec<&'a [u8]>,
121}
122
123/// Format a Format for display
124impl Display for VolumeTableOfContents<'_> {
125    fn fmt(&self, f: &mut Formatter) -> Result {
126        writeln!(
127            f,
128            "track number of first catalog sector: {}",
129            self.track_number_of_first_catalog_sector
130        )?;
131        writeln!(
132            f,
133            "sector number of first catalog sector: {}",
134            self.sector_number_of_first_catalog_sector
135        )?;
136        writeln!(f, "release number of DOS: {}", self.release_number_of_dos)?;
137        writeln!(f, "diskette volume number: {}", self.diskette_volume_number)?;
138
139        writeln!(
140            f,
141            "number of tracks per diskette: {}",
142            self.number_of_tracks_per_diskette
143        )?;
144        writeln!(
145            f,
146            "number of sectors per track: {}",
147            self.number_of_sectors_per_track
148        )?;
149        writeln!(
150            f,
151            "number of bytes per sector: {}",
152            self.number_of_bytes_per_sector
153        )?;
154        writeln!(
155            f,
156            "last_track_where_sectors_were_allocated: {}",
157            self.last_track_where_sectors_were_allocated
158        )
159    }
160}
161
162/// Parse a Volume Table of Contents
163pub fn parse_volume_table_of_contents(i: &[u8]) -> IResult<&[u8], VolumeTableOfContents<'_>> {
164    let (i, reserved) = le_u8(i)?;
165    let (i, track_number_of_first_catalog_sector) = le_u8(i)?;
166    let (i, sector_number_of_first_catalog_sector) = le_u8(i)?;
167    let (i, release_number_of_dos) = le_u8(i)?;
168    let (i, reserved2) = take(2_usize)(i)?;
169    let (i, diskette_volume_number) = le_u8(i)?;
170    let (i, reserved3) = take(32_usize)(i)?;
171    let (i, maximum_number_of_track_sector_pairs) = le_u8(i)?;
172    let (i, reserved4) = take(8_usize)(i)?;
173    let (i, last_track_where_sectors_were_allocated) = le_u8(i)?;
174    let (i, direction_of_track_allocation) = le_i8(i)?;
175    let (i, reserved5) = take(2_usize)(i)?;
176    let (i, number_of_tracks_per_diskette) = le_u8(i)?;
177    let (i, number_of_sectors_per_track) = le_u8(i)?;
178    let (i, number_of_bytes_per_sector) = le_u16(i)?;
179
180    // We'll read in as many as the VTOC says we have tracks, limited
181    // to 50, which stays within the 256-byte limit.
182    let bit_maps_to_read = min(number_of_tracks_per_diskette, 50);
183
184    let (i, bit_map_of_free_sectors) = count(take(4_usize), bit_maps_to_read.into())(i)?;
185
186    Ok((
187        i,
188        VolumeTableOfContents {
189            reserved,
190            track_number_of_first_catalog_sector,
191            sector_number_of_first_catalog_sector,
192            release_number_of_dos,
193            reserved2,
194            diskette_volume_number,
195            reserved3,
196            maximum_number_of_track_sector_pairs,
197            reserved4,
198            last_track_where_sectors_were_allocated,
199            direction_of_track_allocation,
200            reserved5,
201            number_of_tracks_per_diskette,
202            number_of_sectors_per_track,
203            number_of_bytes_per_sector,
204            bit_map_of_free_sectors,
205        },
206    ))
207}
208
209impl SanityCheck for VolumeTableOfContents<'_> {
210    fn check(&self) -> bool {
211        if (self.number_of_tracks_per_diskette != 35) && (self.number_of_tracks_per_diskette != 40)
212        {
213            debug!(
214                "Suspicious number of tracks per diskette: {}",
215                self.number_of_tracks_per_diskette
216            );
217            return false;
218        }
219
220        if (self.number_of_sectors_per_track != 13) && (self.number_of_sectors_per_track != 16) {
221            debug!(
222                "Suspicious number of sectors per track: {}",
223                self.number_of_sectors_per_track
224            );
225            return false;
226        }
227
228        true
229    }
230}
231
232/// An Apple ][ DOS Disk
233pub struct AppleDOSDisk<'a> {
234    /// The Volume Table of Contents
235    pub volume_table_of_contents: VolumeTableOfContents<'a>,
236    /// The disk catalog
237    pub catalog: FullCatalog<'a>,
238    /// Disk tracks.
239    /// Tracks is a vector of sectors, which is a vector of byte
240    /// slices.
241    pub tracks: Vec<Vec<&'a [u8]>>,
242
243    /// The files with data
244    pub files: Files<'a>,
245}
246
247/// The different types of Apple disks
248/// We're ignoring the large_enum_variant warning for now, enum size is still less than
249/// 512 bytes
250/// On normal invocations in the current codebase we only have one
251/// instance of this enum.  Future versions may have more, but for now
252/// the cost is not an issue.
253#[allow(clippy::large_enum_variant)]
254pub enum AppleDiskData<'a> {
255    /// An Apple ][ DOS disk (1.x, 2.x, 3.x)
256    DOS(AppleDOSDisk<'a>),
257    /// An Apple ][ ProDOS disk
258    ProDOS,
259    /// A nibble encoded disk (may contain a DOS image or other data)
260    Nibble(NibbleDisk),
261}
262
263impl DiskImageSaver for AppleDOSDisk<'_> {
264    fn save_disk_image(
265        &self,
266        _config: &Config,
267        selected_filename: Option<&str>,
268        filename: &str,
269    ) -> std::result::Result<(), crate::error::Error> {
270        if selected_filename.is_none() {
271            error!("Filename must be specified for saving Apple DOS 3.3 images");
272            return Err(crate::error::Error::new(ErrorKind::Message(String::from(
273                "Filename must be specified for saving Apple DOS 3.3 images",
274            ))));
275        }
276        let selected_filename = selected_filename.unwrap();
277        let filename = PathBuf::from(filename);
278        let file_result = File::create(filename);
279        match file_result {
280            Ok(mut file) => {
281                let selected_file = self.files.get(selected_filename).unwrap();
282
283                file.write_all(&selected_file.data)?;
284            }
285            Err(e) => error!("Error opening file: {}", e),
286        }
287        Ok(())
288    }
289}
290
291/// An Apple ][ Disk
292pub struct AppleDisk<'a> {
293    /// The disk encoding
294    pub encoding: Encoding,
295    /// The disk format
296    pub format: Format,
297
298    /// The parsed disk data
299    pub data: AppleDiskData<'a>,
300}
301
302/// Format an AppleDisk for display
303impl Display for AppleDisk<'_> {
304    fn fmt(&self, f: &mut Formatter) -> Result {
305        write!(f, "encoding: {}, format: {}", self.encoding, self.format)
306    }
307}
308
309/// Heuristic guesses for what kind of disk this is
310#[derive(Clone, Copy, Debug, Eq, PartialEq)]
311pub struct AppleDiskGuess<'a> {
312    /// The disk encoding
313    pub encoding: Encoding,
314    /// The disk format
315    pub format: Format,
316    /// The raw image data
317    pub data: &'a [u8],
318}
319
320impl<'a> AppleDiskGuess<'a> {
321    /// Return a new AppleDiskGuess with some default parameters that can't
322    /// be easily guessed from basic heuristics like filename
323    pub fn new(encoding: Encoding, format: Format, data: &'a [u8]) -> AppleDiskGuess<'a> {
324        AppleDiskGuess {
325            encoding,
326            format,
327            data,
328        }
329    }
330}
331
332/// Format an AppleDiskGuess for display
333impl Display for AppleDiskGuess<'_> {
334    fn fmt(&self, f: &mut Formatter) -> Result {
335        write!(f, "encoding: {}, format: {}", self.encoding, self.format)
336    }
337}
338
339/// Try to guess a file format from a filename
340impl<'a, 'b> DiskImageGuesser<'a, 'b> for AppleDiskGuess<'a> {
341    fn guess(
342        _config: &crate::config::Config,
343        filename: &str,
344        data: &'a [u8],
345    ) -> Option<DiskImageGuess<'a>> {
346        let filename_extension: Vec<_> = filename.split('.').collect();
347        let path = Path::new(&filename);
348
349        let filesize = match fs::metadata(path) {
350            Ok(metadata) => metadata.len(),
351            Err(e) => {
352                error!("Couldn't get file metadata: {}", e);
353                panic!("Couldn't get file metadata");
354            }
355        };
356
357        match filename_extension[filename_extension.len() - 1]
358            .to_lowercase()
359            .as_str()
360        {
361            // format_from_data does additional checks on common
362            // values in Apple disk images
363            "do" => Some(DiskImageGuess::Apple(AppleDiskGuess::new(
364                Encoding::Plain,
365                Format::DOS33(filesize),
366                data,
367            ))),
368            "dsk" => Some(DiskImageGuess::Apple(AppleDiskGuess::new(
369                Encoding::Plain,
370                Format::DOS33(filesize),
371                data,
372            ))),
373            "nib" => {
374                let prologue_byte_result = recognize_prologue(data);
375                let format = match prologue_byte_result {
376                    Some(r) => match r {
377                        0xB5 => Format::DOS32(filesize),
378                        0x96 => Format::DOS33(filesize),
379                        _ => Format::Unknown(filesize),
380                    },
381                    None => Format::Unknown(filesize),
382                };
383
384                Some(DiskImageGuess::Apple(AppleDiskGuess::new(
385                    Encoding::Nibble,
386                    format,
387                    data,
388                )))
389            }
390            &_ => {
391                // Try using the magic number to identify the file
392                let res = format_from_data(data);
393                match res {
394                    Err(_) => {
395                        info!("Couldn't detect disk type");
396                        None
397                    }
398                    Ok(s) => s,
399                }
400            }
401        }
402    }
403
404    fn parse(
405        &'b self,
406        config: &'a crate::config::Config,
407    ) -> std::result::Result<DiskImage<'a>, Error> {
408        let result = apple_disk_parser(self, config);
409        match result {
410            Ok(res) => Ok(DiskImage::Apple(res)),
411            Err(e) => Err(Error::new(ErrorKind::Invalid(InvalidErrorKind::Invalid(
412                nom::Err::Error(e).to_string(),
413            )))),
414        }
415    }
416}
417
418/// Try to guess a file format from a magic number in the file
419///
420/// # Arguments
421///
422/// * `data` - A u8 slice containing the entire image data to guess
423///
424/// # Returns
425///   Returns a result with an optional AppleDiskGuess, the meaning of this is:
426///      Returns an Err result if there was an error parsing the data.
427///      Returns an Ok result if the parsing was successful, even if
428///      it's not a known image type.
429///        Returns an Ok result with None if the image type is unknown.
430///        Returns an Ok result with an AppleDiskGuess if it's a known type.
431///
432/// There was a design decision here to return None as opposed to an
433/// Unknown Apple image type.  I don't know if it's the right choice.
434pub fn format_from_data(data: &[u8]) -> std::result::Result<Option<DiskImageGuess<'_>>, Error> {
435    let filesize: u64 = data.len().try_into().unwrap();
436
437    info!("Reading magic number from file");
438    let (_i, header) = take(0x09_usize)(data)?;
439
440    if header != [0x01, 0xA5, 0x27, 0xC9, 0x09, 0xD0, 0x18, 0xA5, 0x2B] {
441        return Ok(None);
442    }
443    // Check for an Apple II DOS 3.3 header
444    let (i, _junk) = take(0x11000_usize)(data)?;
445    let (i, _reserved) = le_u8(i)?;
446    let (i, track_number_of_first_catalog_sector) = le_u8(i)?;
447    let (i, sector_number_of_first_catalog_sector) = le_u8(i)?;
448    let (_i, release_number_of_dos) = le_u8(i)?;
449
450    if (track_number_of_first_catalog_sector == 0x11)
451        && (sector_number_of_first_catalog_sector == 0x0F)
452        && (release_number_of_dos == 0x03)
453    {
454        info!("Found Apple DOS 3.3 disk");
455        Ok(Some(DiskImageGuess::Apple(AppleDiskGuess::new(
456            Encoding::Plain,
457            Format::DOS33(filesize),
458            data,
459        ))))
460    } else {
461        Ok(None)
462    }
463}
464
465/// Parse the tracks on an Apple ][ Disk
466pub fn apple_tracks_parser(
467    track_size: usize,
468    number_of_tracks: usize,
469) -> impl Fn(&[u8]) -> IResult<&[u8], Vec<&[u8]>> {
470    move |i| count(take(track_size), number_of_tracks)(i)
471}
472
473/// Parse the tracks on a 140K Apple ][ Disk
474/// This parses the disks and returns the farthest index and a vector
475/// of u8 slices or an error (it actually returns an IResult, which is
476/// composed of this).
477/// The index of the vector is the track number.
478/// Each vector element is 4096 bytes long if tracks_per_disk is 35
479/// It's 143360 / tracks_per_disk for a 140k disk.  That's all the
480/// sectors for that track index.
481pub fn apple_140_k_dos_parser(i: &[u8], tracks_per_disk: usize) -> IResult<&[u8], Vec<&[u8]>> {
482    if tracks_per_disk == 35 {
483        apple_tracks_parser(4096, 35)(i)
484    } else if tracks_per_disk == 40 {
485        apple_tracks_parser(3584, 40)(i)
486    } else {
487        Err(Err::Error(nom::error::Error::new(
488            i,
489            nom::error::ErrorKind::Fail,
490        )))
491    }
492}
493
494/// Parse a DOS 3.3 disk volume
495pub fn volume_parser(i: &[u8], filesize: u64) -> IResult<&[u8], AppleDisk<'_>> {
496    // guess the tracks per disk
497    let tracks_per_disk = 35;
498
499    // guess the starting track for the catalog.
500    // This sometimes starts at other locations.
501    // The variable name is somewhat confusing, it's the track
502    // where the catalog starts.
503    let catalog_sector_start = 17;
504
505    // 140K Apple DOS image
506    // Use the apple_140_k_dos_parser
507    // raw_tracks is a vector of all the tracks, NOT split into
508    // separate sectors
509    let (_i, raw_tracks) = apple_140_k_dos_parser(i, tracks_per_disk)?;
510
511    // Verify that this is the Volume Table of Contents
512    // The catalog should start on sector 17
513    // Sometimes this is zero-based indexing, sometimes it's one-based
514
515    // One heuristic is to check if byte 1 is equal to 17,
516    // the standard track number of the first catalog
517    // sector.
518    // byte 2 is usually equal to 15, the sector number of
519    // the first catalog sector
520    // Another heuristic is to check for a valid DOS release number:
521    // DOS versions to check for: 1, 2, 3
522    let (i, vtoc) = parse_volume_table_of_contents(raw_tracks[catalog_sector_start])?;
523
524    debug!("VTOC: {}", vtoc);
525
526    if !vtoc.check() {
527        error!("Invalid data");
528        return Err(Err::Error(nom::error::Error::new(
529            i,
530            nom::error::ErrorKind::Fail,
531        )));
532    }
533
534    let mut tracks: Vec<Vec<&[u8]>> = Vec::new();
535
536    // parse out the sectors for track 17
537    // This parses through every sector in track catalog_sector_start
538    // and splits it up into 16 sectors of 256 bytes each
539
540    let catalog_sector = raw_tracks[catalog_sector_start][2];
541
542    for track in raw_tracks {
543        let mut track_vec: Vec<&[u8]> = Vec::new();
544        let (_i, sectors) = count(take(256_usize), 16)(track)?;
545        for sector in sectors {
546            track_vec.push(sector);
547        }
548        tracks.push(track_vec);
549    }
550
551    let catalog_res = parse_catalogs(
552        &tracks,
553        catalog_sector_start.try_into().unwrap(),
554        catalog_sector,
555    );
556    let catalog = match catalog_res {
557        Ok(catalog) => catalog,
558        Err(_e) => {
559            return Err(Err::Error(nom::error::Error::new(
560                i,
561                nom::error::ErrorKind::Fail,
562            )));
563        }
564    };
565
566    debug!("Catalog:\n{}", catalog);
567
568    // TODO: Properly convert errors and define an error for this
569    let files = build_files(catalog.clone(), &tracks).unwrap();
570
571    let apple_dos_disk = AppleDOSDisk {
572        volume_table_of_contents: vtoc,
573        catalog,
574        tracks,
575        files,
576    };
577
578    Ok((
579        i,
580        AppleDisk {
581            encoding: Encoding::Plain,
582            format: Format::DOS33(filesize),
583            data: AppleDiskData::DOS(apple_dos_disk),
584        },
585    ))
586}
587
588/// Parse an Apple ][ Disk
589pub fn apple_disk_parser<'a>(
590    guess: &AppleDiskGuess<'a>,
591    config: &Config,
592) -> core::result::Result<AppleDisk<'a>, Error> {
593    debug!("Parsing based on guess: {}", guess);
594
595    match guess.encoding {
596        Encoding::Plain => {
597            let filesize = if let Format::DOS33(size) = guess.format {
598                size
599            } else {
600                0
601            };
602
603            if filesize == 143360 {
604                let res = volume_parser(guess.data, filesize);
605
606                match res {
607                    Ok(ad) => Ok(ad.1),
608                    Err(e) => Err(Error::new(ErrorKind::Invalid(InvalidErrorKind::Invalid(
609                        e.to_string(),
610                    )))),
611                }
612            } else {
613                Err(Error::new(ErrorKind::Invalid(InvalidErrorKind::Invalid(
614                    String::from("Test1"),
615                ))))
616            }
617        }
618        Encoding::Nibble => {
619            debug!("Parsing as nibble format");
620            let (_i, disk) = parse_nib_disk(config)(guess.data)?;
621
622            Ok(AppleDisk {
623                encoding: guess.encoding,
624                format: guess.format,
625                data: AppleDiskData::Nibble(disk),
626            })
627        }
628    }
629}
630
631/// DiskImageParser implementation for AppleDiskGuess
632impl<'a> DiskImageParser<'a, '_> for AppleDiskGuess<'a> {
633    fn parse_disk_image(
634        &'a self,
635        config: &'a crate::config::Config,
636        _filename: &str,
637    ) -> std::result::Result<DiskImage<'a>, Error> {
638        info!("DiskImageParser Attempting to parse Apple disk");
639        let result = apple_disk_parser(self, config);
640        match result {
641            Ok(apple_disk) => Ok(DiskImage::Apple(apple_disk)),
642            Err(e) => Err(Error::new(ErrorKind::Invalid(InvalidErrorKind::Invalid(
643                nom::Err::Error(e).to_string(),
644            )))),
645        }
646    }
647}
648
649impl<'a, 'b> DiskImageOps<'a, 'b> for AppleDiskGuess<'a> {
650    fn catalog(&'a self, _config: &'b crate::config::Config) -> std::result::Result<String, Error> {
651        info!("DiskImageParser catalog unimplemented for Apple disk");
652        Err(Error::new(ErrorKind::Invalid(InvalidErrorKind::Invalid(
653            String::from("DiskImageParser catalog unimplemented for Apple disk"),
654        ))))
655    }
656}
657
658#[cfg(test)]
659mod tests {
660    use std::fs::OpenOptions;
661    use std::io::Write;
662    use std::path::Path;
663
664    use crate::disk_format::image::{format_from_filename_and_data, DiskImageGuess};
665
666    use super::{
667        apple_disk_parser, format_from_data, parse_volume_table_of_contents, AppleDiskData,
668        AppleDiskGuess, Encoding, Format,
669    };
670    use crate::config::{Config, Configuration};
671
672    const VTOC_DATA: [u8; 256] = [
673        0x00, 0x11, 0x0F, 0x03, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
674        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
675        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00,
676        0x00, 0x00, 0x00, 0x12, 0x01, 0x00, 0x00, 0x23, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
677        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00,
678        0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF,
679        0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF,
680        0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
681        0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00,
682        0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF,
683        0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF,
684        0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
685        0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00,
686        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
687        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
688        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
689        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
690        0x00,
691    ];
692
693    /// Try testing format_from_filename_and_data
694    #[test]
695    fn format_from_filename_works() {
696        let filename = "testdata/test-disk_format_from_filename_works.dsk";
697
698        /* Version where we build the file in the test instead of
699         * saving it to version control */
700        let path = Path::new(&filename);
701        let mut file = OpenOptions::new()
702            .create(true)
703            .truncate(true)
704            .write(true)
705            .open(path)
706            .unwrap_or_else(|e| {
707                panic!("Couldn't open file: {}", e);
708            });
709        let data: [u8; 143360] = [0; 143360];
710
711        file.write_all(&data).unwrap_or_else(|e| {
712            panic!("Error writing test file: {}", e);
713        });
714        file.flush().unwrap_or_else(|e| {
715            panic!("Couldn't flush file stream: {}", e);
716        });
717
718        let config = Config::load(config::Config::default()).unwrap();
719        let guess = format_from_filename_and_data(&config, filename, &data).unwrap_or_else(|| {
720            panic!("Invalid filename guess");
721        });
722
723        assert_eq!(
724            guess,
725            DiskImageGuess::Apple(AppleDiskGuess::new(
726                Encoding::Plain,
727                Format::DOS33(143360),
728                &data
729            ))
730        );
731
732        std::fs::remove_file(filename).unwrap_or_else(|e| {
733            panic!("Error removing test file: {}", e);
734        });
735    }
736
737    /// Try testing format_from_data
738    /// Right now, this only tests for Apple DOS 3.3 disk images using
739    /// magic numbers at the start and in the VTOC
740    #[test]
741    fn format_from_data_works() {
742        let mut data: [u8; 143360] = [0; 143360];
743        let magic_number_start = [0x01, 0xA5, 0x27, 0xC9, 0x09, 0xD0, 0x18, 0xA5, 0x2B];
744        let magic_number_vtoc = [0x11, 0x0F, 0x03];
745
746        data[0..9].copy_from_slice(&magic_number_start);
747        data[0x11001..0x11004].copy_from_slice(&magic_number_vtoc);
748
749        let guess_res = format_from_data(&data);
750        match guess_res {
751            Ok(g) => {
752                if let Some(guess) = g {
753                    assert_eq!(
754                        guess,
755                        DiskImageGuess::Apple(AppleDiskGuess::new(
756                            Encoding::Plain,
757                            Format::DOS33(143360),
758                            &data
759                        ))
760                    );
761                } else {
762                    panic!("Invalid data guess");
763                }
764            }
765            Err(_) => {
766                panic!("Data guess failed");
767            }
768        };
769    }
770
771    /// Try testing format_from_data fails with wrong magic number
772    ///
773    /// Right now, this only tests for Apple DOS 3.3 disk images using
774    /// magic numbers at the start and in the VTOC
775    ///
776    /// This test should fail with invalid VTOC for DOS 3.3
777    ///
778    /// It may need to be changed when other filesystem guesses are
779    /// impelemented, so it will be hopefully be helpful for the next
780    /// maintainer or developer.
781    #[test]
782    fn format_from_data_fails() {
783        let mut data: [u8; 143360] = [0; 143360];
784        let magic_number_start = [0x01, 0xA5, 0x27, 0xC9, 0x09, 0xD0, 0x18, 0xA5, 0x2B];
785
786        data[0..9].copy_from_slice(&magic_number_start);
787
788        let guess_res = format_from_data(&data);
789        match guess_res {
790            Ok(g) => {
791                if let Some(guess) = g {
792                    assert_ne!(
793                        guess,
794                        DiskImageGuess::Apple(AppleDiskGuess::new(
795                            Encoding::Plain,
796                            Format::DOS33(143360),
797                            &data
798                        ))
799                    );
800                } else {
801                    assert_eq!(g, None, "Correct data guess for invalid DOS 3.3 data");
802                }
803            }
804            Err(_) => {
805                panic!("Data guess failed");
806            }
807        };
808    }
809
810    /// Test parsing a Volume Table of Contents
811    #[test]
812    fn parse_volume_table_of_contents_works() {
813        let vtoc_data = VTOC_DATA;
814
815        let result = parse_volume_table_of_contents(&vtoc_data);
816
817        match result {
818            Ok(vtoc) => {
819                assert_eq!(vtoc.1.track_number_of_first_catalog_sector, 17);
820                assert_eq!(vtoc.1.sector_number_of_first_catalog_sector, 15);
821                assert_eq!(vtoc.1.release_number_of_dos, 3);
822                assert_eq!(vtoc.1.diskette_volume_number, 254);
823                assert_eq!(vtoc.1.number_of_tracks_per_diskette, 35);
824                assert_eq!(vtoc.1.number_of_sectors_per_track, 16);
825                assert_eq!(vtoc.1.number_of_bytes_per_sector, 256);
826                assert_eq!(vtoc.1.last_track_where_sectors_were_allocated, 18);
827            }
828            Err(e) => {
829                panic!("Couldn't parse VTOC: {}", e);
830            }
831        }
832    }
833
834    /// Test parsing a non-standard Apple ][ DOS 3.3 disk
835    /// A lot of these disks have custom code to and different locations for the VTOC
836    /// Test collecting heuristics on Apple disk images
837    #[test]
838    fn apple_disk_parser_disk_works() {
839        let filename = "testdata/test-apple_disk_parser_works.dsk";
840        let path = Path::new(&filename);
841
842        let mut data: Vec<u8> = Vec::new();
843        let data_prefix: [u8; 0x11000] = [0; 0x11000];
844        let data_vtoc = VTOC_DATA;
845        let data_suffix: [u8; 0x11F00] = [0; 0x11F00];
846
847        data.extend(data_prefix);
848        data.extend(data_vtoc);
849        data.extend(data_suffix);
850
851        std::fs::write(path, &data).unwrap_or_else(|e| {
852            panic!("Error writing test file: {}", e);
853        });
854
855        // This may not work on GitHub Actions due to their CI
856        // environment restrictions
857        // let guess = format_from_filename(filename).unwrap_or_else(|| {
858        //     panic!("Invalid filename guess");
859        // });
860
861        let guess = AppleDiskGuess::new(Encoding::Plain, Format::DOS33(143360), &data);
862
863        let config = Config::load(config::Config::default()).unwrap();
864        let res = apple_disk_parser(&guess, &config);
865
866        match res {
867            Ok(disk) => match disk.data {
868                AppleDiskData::DOS(apple_dos_disk) => {
869                    let vtoc = apple_dos_disk.volume_table_of_contents;
870
871                    assert_eq!(disk.encoding, Encoding::Plain);
872                    assert_eq!(disk.format, Format::DOS33(143360));
873                    assert_eq!(vtoc.track_number_of_first_catalog_sector, 17);
874                    assert_eq!(vtoc.sector_number_of_first_catalog_sector, 15);
875                    assert_eq!(vtoc.release_number_of_dos, 3);
876                    assert_eq!(vtoc.diskette_volume_number, 254);
877                    assert_eq!(vtoc.number_of_tracks_per_diskette, 35);
878                    assert_eq!(vtoc.number_of_sectors_per_track, 16);
879                    assert_eq!(vtoc.number_of_bytes_per_sector, 256);
880                    assert_eq!(vtoc.last_track_where_sectors_were_allocated, 18);
881                }
882                _ => {
883                    panic!("Invalid format");
884                }
885            },
886            Err(_e) => {
887                panic!("This should have succeeded");
888            }
889        }
890
891        std::fs::remove_file(filename).unwrap_or_else(|e| {
892            panic!("Error removing test file: {}", e);
893        });
894    }
895
896    /// Test parsing a non-standard Apple ][ DOS 3.3 disk
897    /// A lot of these disks have custom code to and different locations for the VTOC
898    /// Test collecting heuristics on Apple disk images
899    #[test]
900    fn apple_disk_parser_nonstandard_disk_panics() {
901        let filename = "testdata/test-apple_disk_parser_nonstandard_disk_panics.dsk";
902
903        /* Version where we build the file in the test instead of
904         * saving it to version control */
905        let path = Path::new(&filename);
906        let data: [u8; 143360] = [0; 143360];
907        std::fs::write(path, data).unwrap_or_else(|e| {
908            panic!("Error writing test file: {}", e);
909        });
910
911        let guess = AppleDiskGuess::new(Encoding::Plain, Format::DOS33(143360), &data);
912
913        let config = Config::load(config::Config::default()).unwrap();
914        let res = apple_disk_parser(&guess, &config);
915
916        match res {
917            Ok(_disk) => {
918                panic!("This should have failed parsing");
919            }
920            Err(_e) => {
921                // Check for more specific error result
922                assert_eq!(true, true);
923            }
924        }
925
926        std::fs::remove_file(filename).unwrap_or_else(|e| {
927            panic!("Error removing test file: {}", e);
928        });
929    }
930}