Skip to main content

image_rider/disk_format/
image.rs

1//! The image_rider::disk_format::image module provides a set of
2//! common functions and trait definitions for reading disks and
3//! cartridges.
4use log::info;
5
6use nom::branch::alt;
7use nom::combinator::map;
8use nom::IResult;
9use std::fmt::{Display, Formatter, Result};
10
11use crate::{
12    disk_format::{
13        apple::{
14            self,
15            disk::{AppleDisk, AppleDiskData, AppleDiskGuess},
16        },
17        stx::disk::{stx_disk_parser, STXDisk, STXDiskGuess},
18    },
19    error::{Error, ErrorKind},
20    init,
21};
22
23#[cfg(feature = "commodore")]
24use crate::disk_format::commodore::{
25    d64::{d64_disk_parser, D64Disk},
26    disk::CommodoreDiskGuess,
27};
28
29/// DiskImage is the primary enumeration for holding disk images.
30///
31/// The DiskImageParser and DiskImageSaver trait functions return and
32/// operate on this enumeration.
33///
34/// Because the Disk data structures are more intelligent than simple
35/// byte-oriented C structures, copying them isn't as easy as copying
36/// a block of bytes.
37/// rust-clippy recommends boxing the large fields to reduce the total size of the enum.
38/// This is a new recommendation, we'll ignore it for now and investigate other solution.
39/// DiskImage construction is usually done once at the beginning of the program,
40/// and total variant size is still around ~512 bytes
41/// On normal invocations in the current codebase we only have one
42/// instance of this enum.  Future versions may have more, but for now
43/// the cost is not an issue.
44/// If this code is adapted to process a large number of images and
45/// thrashing is a concern, feel free to fix it.
46#[allow(clippy::large_enum_variant)]
47pub enum DiskImage<'a> {
48    /// A Commodore 64 D64 Disk Image
49    #[cfg(feature = "commodore")]
50    D64(D64Disk<'a>),
51    /// An Atari ST STX Disk Image.
52    /// Usually the raw data in a STX disk image is a FAT12 filesystem.
53    STX(STXDisk<'a>),
54    /// An Apple ][ Disk Image There are several different encodings,
55    /// formats, and filesystems for Apple2 disks.  This includes
56    /// nibble encoding and DOS 3.x and ProDOS filesystems.
57    Apple(AppleDisk<'a>),
58}
59
60/// Display a DiskImage
61impl Display for DiskImage<'_> {
62    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
63        match self {
64            #[cfg(feature = "commodore")]
65            DiskImage::D64(_) => write!(f, "D64 Disk"),
66            DiskImage::STX(_) => write!(f, "STX Disk"),
67            DiskImage::Apple(d) => write!(f, "Apple Disk: {}", d),
68        }
69    }
70}
71
72/// Set of operations for parsed disk images
73/// This includes things like getting the catalog
74pub trait DiskImageOps<'a, 'b> {
75    /// This function lists the contents of the disk
76    /// It simply returns the root directory as a string
77    /// Future versions could provide more structure
78    ///
79    /// # Arguments
80    ///
81    /// - `config` - A Config object that contains information to guide parsing.
82    /// - `filename` - The name of the file to list.
83    ///
84    /// # Returns
85    ///
86    /// A Result containing the Disk contents as a String
87    ///
88    /// # Examples
89    /// ```no_run
90    /// // Start of setup code
91    /// use image_rider::{
92    ///       config::{Config, Configuration},
93    ///       disk_format::image::{DiskImageOps, DiskImageParser},
94    /// };
95    ///
96    /// let filename = "parse_disk_image-tmpfile-disk_image_ops-catalog.img";
97    /// let data: Vec<u8> = Vec::new();
98    /// let settings = config::Config::builder().build().unwrap();
99    /// let config = Config::load(settings).expect("Error loading image rider config");
100    /// // End of the setup code
101    ///
102    /// // The main method call
103    /// let result = data.parse_disk_image(
104    ///     &config,
105    ///     &filename
106    /// );
107    /// let result = result.unwrap().catalog(&config);
108    /// if let Ok(catalog) = result {
109    ///     println!("Successful list of disk contents");
110    /// }
111    /// ```
112    fn catalog(&'a self, config: &'b crate::config::Config) -> std::result::Result<String, Error>;
113}
114
115/// A trait for disk or ROM image parsers
116/// New image guessers should implement this trait
117/// It's also implemented for &[u8]
118///
119/// The disk parsing and the disk image access (data, saving, etc.)
120/// functions have been moved into separate traits.  This supports the
121/// new data flow in the library.
122/// It's assumed that image type is guessed from raw image data and a
123/// DiskImageGuess structure is created.
124/// From this structure, an image can be parsed.
125///
126/// It allows easy guiding of the parsing from the command line,
127/// just specify the file type on the command line, along with guesses on
128/// things like directory table locations and an DiskImageGuess can be generated.
129///
130/// The DiskImageParser trait and DiskImageSaver trait are the primary
131/// API access points to the image-rider crate.  Application
132/// developers should use these two traits to load and store data in
133/// their application.
134///
135/// The individual DiskImage structures provide additional fields for
136/// users familiar with a specific image type.
137pub trait DiskImageParser<'a, 'b> {
138    /// This function parses an entire disk, returning a DiskImage
139    ///
140    /// # Arguments
141    ///
142    /// - `config` - A Config object that contains information to guide parsing.
143    /// - `filename` - The name of the file to parse.
144    ///
145    /// # Returns
146    ///
147    /// A Result containing the DiskImage or an Error.
148    ///
149    /// # Examples
150    /// ```no_run
151    /// // Start of setup code
152    /// use std::path::Path;
153    /// use std::io::Read;
154    /// use std::fs::{File, OpenOptions};
155    /// use image_rider::config::{Config, Configuration};
156    /// use image_rider::disk_format::image::DiskImageParser;
157    ///
158    /// let filename = "parse_disk_image-tmpfile-disk_image_parser-parse_disk_image.img";
159    /// let path = Path::new(&filename);
160    /// let mut file = OpenOptions::new()
161    ///     .create(true)
162    ///     .write(true)
163    ///     .open(path)
164    ///     .unwrap_or_else(|e| {
165    ///         panic!("Couldn't open file: {}", e);
166    ///     });
167    /// let data: Vec<u8> = Vec::new();
168    /// let settings = config::Config::builder().build().unwrap();
169    /// let config = image_rider::config::Config::load(settings).expect("Error loading image rider config");
170    /// // End of the setup code
171    ///
172    /// // The main method call
173    /// let result = data.parse_disk_image(
174    ///     &config,
175    ///     &filename
176    /// );
177    /// if let Ok(disk_image) = result {
178    ///     println!("Successful parse");
179    /// }
180    ///
181    /// // Teardown code
182    /// std::fs::remove_file(filename).unwrap_or_else(|e| {
183    ///         panic!("Error removing test file: {}", e);
184    /// });
185    ///
186    /// ```
187    fn parse_disk_image(
188        &'a self,
189        config: &'a crate::config::Config,
190        filename: &str,
191    ) -> std::result::Result<DiskImage<'a>, Error>;
192}
193
194/// This trait provides convenient functions for getting and saving
195/// data for the parsed disk image data in a DiskImage
196pub trait DiskImageSaver {
197    // /// Return the primary data contents of a disk image
198    // /// The meaning of the data contents will differ between image formats, but
199    // /// it's usually all the volume, track, and sector data, or the enclosed file format
200    // /// if the outer image is a wrapper
201    // fn disk_image_data(&self, config: &Config) -> Vec<&[u8]>;
202    //
203    /// Save the primary data contents of a disk image to disk
204    /// The meaning of the data contents will differ between image formats, but
205    /// it's usually all the volume, track, and sector data, or the enclosed file format
206    /// if the outer image is a wrapper.
207    /// This function parses an entire disk, returning a DiskImage.
208    ///
209    /// # Arguments
210    ///
211    /// - `config` - A Config object that contains information to guide parsing.
212    /// - `filename` - The name of the file to parse.
213    ///
214    /// # Examples
215    /// ```no_run
216    /// // Start of setup code
217    /// use std::path::Path;
218    /// use std::io::Read;
219    /// use std::fs::{File, OpenOptions};
220    /// use image_rider::config::{Config, Configuration};
221    /// use image_rider::disk_format::image::{DiskImageParser, DiskImageSaver};
222    ///
223    /// let filename = "parse_disk_image-tmpfile-disk_image_saver-save_disk_image.img";
224    /// let path = Path::new(&filename);
225    /// let mut file = OpenOptions::new()
226    ///     .create(true)
227    ///     .write(true)
228    ///     .open(path)
229    ///     .unwrap_or_else(|e| {
230    ///         panic!("Couldn't open file: {}", e);
231    ///     });
232    /// let data: Vec<u8> = Vec::new();
233    /// let settings = config::Config::builder().build().unwrap();
234    /// let config = image_rider::config::Config::load(settings).expect("Error loading image rider config");
235    /// // End of the setup code
236    ///
237    /// // The main method call
238    /// let result = data.parse_disk_image(
239    ///     &config,
240    ///     &filename
241    /// );
242    /// let tmp_out_filename = "parse_disk_image-tmpfile-out-disk_image_saver-save_disk_image-tmp_out.img";
243    /// if let Ok(disk_image) = result {
244    ///     println!("Successful parse");
245    ///     // Save the data
246    ///     disk_image.save_disk_image(&config, None, tmp_out_filename);
247    /// }
248    ///
249    /// // Teardown code
250    /// std::fs::remove_file(tmp_out_filename).unwrap_or_else(|e| {
251    ///         println!("Error removing test file: {}", e);
252    /// });
253    /// std::fs::remove_file(filename).unwrap_or_else(|e| {
254    ///         println!("Error removing test file: {}", e);
255    /// });
256    ///
257    /// ```
258    fn save_disk_image(
259        &self,
260        config: &crate::config::Config,
261        selected_filename: Option<&str>,
262        filename: &str,
263    ) -> std::result::Result<(), crate::error::Error>;
264}
265
266/// The result of heuristics to guess a disk image
267/// Certain disk images can be guessed accurately based on filenames
268/// This returns a guess that can be used to guide the parsing process
269/// Later versions can include a parser generator trait that returns the recommended
270/// parser
271/// The DiskImageGuess structures should have a field that contains
272/// the raw image data When A DiskImageGuess is created, it becomes
273/// the new owner of the image data
274#[derive(Clone, Copy, Debug, Eq, PartialEq)]
275pub enum DiskImageGuess<'a> {
276    /// A Commodore D64 Disk Image
277    #[cfg(feature = "commodore")]
278    Commodore(CommodoreDiskGuess<'a>),
279    /// An Atari ST STX Disk Image
280    STX(STXDiskGuess<'a>),
281    /// An Apple ][ Disk Image
282    Apple(AppleDiskGuess<'a>),
283}
284
285/// This trait defines some functions for guessing disk image types
286/// based on heuristics such as filename and magic numbers.
287pub trait DiskImageGuesser<'a, 'b> {
288    /// Guess an image format from a filename and file data.  Builds
289    /// and returns a DiskImageGuess for a given filename and file
290    /// data.
291    ///
292    /// # Arguments
293    ///
294    /// - `filename` - The name of the file to generate a guess for.
295    /// - `data` - The disk image data as a reference to a byte array.
296    ///
297    /// # Returns
298    ///
299    /// An Option containing the DiskImageGuess if a format was
300    /// identified, or None if none was identified.
301    fn guess(
302        config: &crate::config::Config,
303        filename: &str,
304        data: &'a [u8],
305    ) -> Option<DiskImageGuess<'a>>;
306
307    /// Parse an entire disk, returning a DiskImage.
308    ///
309    /// # Arguments
310    ///
311    /// - `config` - A Config object that contains information to guide parsing.
312    /// - `filename` - The name of the file to parse.
313    ///
314    /// # Returns
315    ///
316    /// A Result containing the DiskImage or an error message.
317    ///
318    fn parse(
319        &'b self,
320        config: &'a crate::config::Config,
321    ) -> std::result::Result<DiskImage<'a>, Error>;
322}
323
324/// Display a DiskImageGuess
325impl Display for DiskImageGuess<'_> {
326    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
327        match self {
328            #[cfg(feature = "commodore")]
329            DiskImageGuess::Commodore(_) => write!(f, "Commodore Disk"),
330            DiskImageGuess::STX(_) => write!(f, "STX Disk"),
331            DiskImageGuess::Apple(d) => write!(f, "Apple Disk: {}", d),
332        }
333    }
334}
335
336/// Implement a parser for a DiskImageGuess
337/// The intention is that the DiskImage owns the raw data afterwards
338impl<'a> DiskImageParser<'a, '_> for DiskImageGuess<'a> {
339    fn parse_disk_image(
340        &'a self,
341        config: &'a crate::config::Config,
342        _filename: &str,
343    ) -> std::result::Result<DiskImage<'a>, crate::error::Error> {
344        // Initialize the image-rider module
345        init();
346
347        match self {
348            #[cfg(feature = "commodore")]
349            DiskImageGuess::Commodore(_guess) => Err(Error::new(ErrorKind::Unimplemented(
350                String::from("Error parsing image from guess"),
351            ))),
352            DiskImageGuess::STX(_) => Err(Error::new(ErrorKind::Unimplemented(String::from(
353                "Error parsing image from guess",
354            )))),
355            DiskImageGuess::Apple(guess) => {
356                let res = guess.parse(config);
357                res
358            }
359        }
360    }
361}
362
363impl DiskImageSaver for DiskImage<'_> {
364    fn save_disk_image(
365        &self,
366        config: &crate::config::Config,
367        selected_filename: Option<&str>,
368        filename: &str,
369    ) -> std::result::Result<(), crate::error::Error> {
370        match self {
371            DiskImage::STX(image_data) => {
372                image_data.save_disk_image(config, None, filename)?;
373                Ok(())
374            }
375            DiskImage::Apple(apple_image) => match &apple_image.data {
376                AppleDiskData::Nibble(nibble_image) => {
377                    nibble_image.save_disk_image(config, None, filename)?;
378                    Ok(())
379                }
380                AppleDiskData::DOS(dos_image) => {
381                    info!("Saving DOS 3.3 file");
382                    dos_image.save_disk_image(config, selected_filename, filename)?;
383                    Ok(())
384                }
385                _ => {
386                    info!("Unsupported image for file saving");
387                    Err(crate::error::Error::new(
388                        crate::error::ErrorKind::Unimplemented(String::from(
389                            "Saving unknown Apple disk images not implemented\n",
390                        )),
391                    ))
392                }
393            },
394            #[cfg(feature = "commodore")]
395            _ => {
396                info!("Unsupported image for file saving");
397                Err(crate::error::Error::new(
398                    crate::error::ErrorKind::Unimplemented(String::from(
399                        "Saving unknown disk images not implemented\n",
400                    )),
401                ))
402            }
403        }
404    }
405}
406
407impl<'a, 'b> DiskImageOps<'a, 'b> for DiskImage<'a> {
408    fn catalog(
409        &'a self,
410        #[cfg(feature = "commodore")] config: &'b crate::config::Config,
411        #[cfg(not(feature = "commodore"))] _config: &'b crate::config::Config,
412    ) -> std::result::Result<String, crate::error::Error> {
413        match self {
414            // DiskImage::Apple(apple_image) => {
415            // 	apple_image.catalog(config)
416            // },
417            #[cfg(feature = "commodore")]
418            DiskImage::D64(d64_image) => d64_image.catalog(config),
419            // DiskImage::STX(image_data) => {
420            //     image_data.catalog(config)
421            // }
422            _ => {
423                info!("Unsupported image for file saving");
424                Err(crate::error::Error::new(
425                    crate::error::ErrorKind::Unimplemented(String::from(
426                        "Saving unknown disk images not implemented\n",
427                    )),
428                ))
429            }
430        }
431    }
432}
433
434/// Parses a file given a filename, returning a DiskImage
435pub fn file_parser<'a>(
436    filename: &str,
437    data: &'a [u8],
438    config: &'a crate::config::Config,
439) -> std::result::Result<DiskImage<'a>, Error> {
440    let guess_image_type = format_from_filename_and_data(config, filename, data);
441
442    info!(
443        "config ignore-checksums: {:?}",
444        config.settings.get_bool("ignore-checksums")
445    );
446
447    match guess_image_type {
448        Some(i) => match i {
449            DiskImageGuess::Apple(guess) => {
450                // Before this can be refactored to the
451                // DiskImageParser trait, the code needs to be
452                // rewritten to transfer ownership from
453                // the DiskImageGuess to the DiskImage
454                info!("Attempting to parse Apple disk");
455                let res = guess.parse(config);
456                res
457            }
458            #[cfg(feature = "commodore")]
459            DiskImageGuess::Commodore(guess) => {
460                info!("Attempting to parse Commodore disk");
461                let res = guess.parse(config);
462                res
463            }
464            // DiskImageGuess::STX(guess) => {
465            // 	info!("Attempting to parse Atari disk");
466            // 	guess.parse(config)
467            // },
468            // None => todo!(),
469            _ => panic!("Exiting"),
470        },
471        None => todo!(), // disk_image_parser(config)(data),
472    }
473}
474
475/// Parse a disk image
476/// This attempts to parse the different file types supported by this library
477/// It returns the remaining input and a DiskImage
478pub fn disk_image_parser<'a>(
479    #[cfg(feature = "commodore")] config: &'a crate::config::Config,
480    #[cfg(not(feature = "commodore"))] _config: &'a crate::config::Config,
481) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], DiskImage<'a>> {
482    move |i| {
483        // Assume the alt parser is greedy and checks the next parser
484        // on the first error
485        alt((
486            #[cfg(feature = "commodore")]
487            map(d64_disk_parser(config), DiskImage::D64),
488            map(stx_disk_parser, DiskImage::STX),
489        ))(i)
490    }
491}
492
493// /// Implementation of DiskImageParser for references to 8-bit integer arrays
494// impl<'a, 'b> DiskImageParser<'a, 'b> for &[u8] {
495//     fn parse_disk_image(
496//         self,
497//         config: &'b Config,
498//         filename: &str,
499//     ) -> IResult<&'a [u8], DiskImage<'a>> {
500//         file_parser(filename, self, config)
501//     }
502// }
503/// Implementation of DiskImageParser for 8-bit integer vectors
504/// TODO: parse_disk_image should be a trait function for DiskImageGuess types
505/// TODO: catalog should be a trait function for DiskImage types
506impl<'a> DiskImageParser<'a, '_> for Vec<u8> {
507    fn parse_disk_image(
508        &'a self,
509        config: &'a crate::config::Config,
510        filename: &str,
511    ) -> std::result::Result<DiskImage<'a>, Error> {
512        // Initialize the image-rider module
513        init();
514
515        file_parser(filename, self.as_slice(), config)
516    }
517}
518
519/// Guess an image format from a filename.  Builds and returns a
520/// DiskImageGuess for a given filename and file data.
521///
522/// # Arguments
523///
524/// - `filename` - The name of the file to generate a guess for.
525/// - `data` - The disk image data as a reference to a byte array.
526///
527/// # Returns
528///
529/// An Option containing the DiskImageGuess
530pub fn format_from_filename_and_data<'a>(
531    config: &crate::config::Config,
532    filename: &str,
533    data: &'a [u8],
534) -> Option<DiskImageGuess<'a>> {
535    info!("Guessing image format from filename and data");
536
537    // TODO: format_from_filename should be defined by a trait, and
538    // each module should expose a type that implements that trait
539    // let apple_res = apple::disk::format_from_filename_and_data(filename, data);
540
541    // I'm sure one of the chaining Option methods lets us just find
542    // the "first" Some in a list of calls, but I don't know which
543    // one.
544
545    #[cfg(feature = "commodore")]
546    let commodore_res = CommodoreDiskGuess::guess(config, filename, data);
547
548    #[cfg(feature = "commodore")]
549    if commodore_res.is_some() {
550        return commodore_res;
551    }
552
553    let apple_res = apple::disk::AppleDiskGuess::guess(config, filename, data);
554
555    apple_res
556}
557
558/// Function to collect the actual disk image data from a disk image and return
559/// it as an `Option<Vec<u8>>`
560/// It should have more tests around the different disk types
561pub fn disk_image_data(disk_image: &DiskImage) -> Option<Vec<u8>> {
562    match disk_image {
563        DiskImage::STX(image_data) => {
564            // It may be more efficient to return sector-size &[u8] iterators
565            Some(
566                image_data
567                    .stx_tracks
568                    .iter()
569                    .filter(|s| s.sector_data.is_some())
570                    .flat_map(|s| s.sector_data.as_ref().unwrap().iter())
571                    .flat_map(|bytes| (*bytes).iter())
572                    .copied()
573                    .collect(),
574            )
575        }
576        _ => {
577            info!("Unsupported image for file saving");
578            None
579        }
580    }
581}
582
583#[cfg(test)]
584mod tests {
585    use std::fs::OpenOptions;
586    use std::io::Write;
587    use std::path::Path;
588
589    use super::apple::disk::{Encoding, Format};
590    use super::AppleDiskGuess;
591    use super::{format_from_filename_and_data, DiskImageGuess};
592    use crate::config::{Config, Configuration};
593
594    /// Test collecting heuristics on disk image type
595    #[test]
596    fn format_from_filename_works() {
597        let filename = "testdata/test-image_format_from_filename_works.dsk";
598
599        /* Version where we build the file in the test instead of
600         * saving it to version control */
601        let path = Path::new(&filename);
602        let mut file = OpenOptions::new()
603            .create(true)
604            .truncate(true)
605            .write(true)
606            .open(path)
607            .unwrap_or_else(|e| {
608                panic!("Couldn't open file: {}", e);
609            });
610        let data: [u8; 143360] = [0; 143360];
611
612        file.write_all(&data).unwrap_or_else(|e| {
613            panic!("Error writing test file: {}", e);
614        });
615        file.flush().unwrap_or_else(|e| {
616            panic!("Couldn't flush file stream: {}", e);
617        });
618
619        let config = Config::load(config::Config::default()).unwrap();
620
621        let guess = format_from_filename_and_data(&config, filename, &data).unwrap_or_else(|| {
622            panic!("Invalid filename guess");
623        });
624
625        match guess {
626            DiskImageGuess::Apple(g) => {
627                assert_eq!(
628                    g,
629                    AppleDiskGuess::new(Encoding::Plain, Format::DOS33(143360), &data)
630                );
631            }
632            _ => {
633                panic!("Invalid filename guess");
634            }
635        }
636
637        std::fs::remove_file(filename).unwrap_or_else(|e| {
638            panic!("Error removing test file: {}", e);
639        });
640    }
641}