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}