use log::{debug, error, info, warn};
use std::{
cmp::min,
fs::{self, File},
io::Write,
path::{Path, PathBuf},
};
use config::Config;
use nom::bytes::complete::take;
use nom::multi::count;
use nom::number::complete::{le_i8, le_u16, le_u8};
use nom::{Err, IResult};
use std::fmt::{Display, Formatter, Result};
use crate::disk_format::apple::catalog::{build_files, parse_catalogs, Files, FullCatalog};
use crate::disk_format::apple::nibble::{parse_nib_disk, recognize_prologue};
use crate::disk_format::image::{DiskImage, DiskImageParser, DiskImageSaver};
use crate::disk_format::sanity_check::SanityCheck;
use crate::error::{Error, ErrorKind, InvalidErrorKind};
use super::nibble::NibbleDisk;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Encoding {
Plain,
Nibble,
}
impl Display for Encoding {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{:?}", self)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Format {
Unknown(u64),
DOS32(u64),
DOS33(u64),
ProDOS(u64),
}
impl Display for Format {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{:?}", self)
}
}
pub struct VolumeTableOfContents<'a> {
pub reserved: u8,
pub track_number_of_first_catalog_sector: u8,
pub sector_number_of_first_catalog_sector: u8,
pub release_number_of_dos: u8,
pub reserved2: &'a [u8],
pub diskette_volume_number: u8,
pub reserved3: &'a [u8],
pub maximum_number_of_track_sector_pairs: u8,
pub reserved4: &'a [u8],
pub last_track_where_sectors_were_allocated: u8,
pub direction_of_track_allocation: i8,
pub reserved5: &'a [u8],
pub number_of_tracks_per_diskette: u8,
pub number_of_sectors_per_track: u8,
pub number_of_bytes_per_sector: u16,
pub bit_map_of_free_sectors: Vec<&'a [u8]>,
}
impl Display for VolumeTableOfContents<'_> {
fn fmt(&self, f: &mut Formatter) -> Result {
writeln!(
f,
"track number of first catalog sector: {}",
self.track_number_of_first_catalog_sector
)?;
writeln!(
f,
"sector number of first catalog sector: {}",
self.sector_number_of_first_catalog_sector
)?;
writeln!(f, "release number of DOS: {}", self.release_number_of_dos)?;
writeln!(f, "diskette volume number: {}", self.diskette_volume_number)?;
writeln!(
f,
"number of tracks per diskette: {}",
self.number_of_tracks_per_diskette
)?;
writeln!(
f,
"number of sectors per track: {}",
self.number_of_sectors_per_track
)?;
writeln!(
f,
"number of bytes per sector: {}",
self.number_of_bytes_per_sector
)?;
writeln!(
f,
"last_track_where_sectors_were_allocated: {}",
self.last_track_where_sectors_were_allocated
)
}
}
pub fn parse_volume_table_of_contents(i: &[u8]) -> IResult<&[u8], VolumeTableOfContents> {
let (i, reserved) = le_u8(i)?;
let (i, track_number_of_first_catalog_sector) = le_u8(i)?;
let (i, sector_number_of_first_catalog_sector) = le_u8(i)?;
let (i, release_number_of_dos) = le_u8(i)?;
let (i, reserved2) = take(2_usize)(i)?;
let (i, diskette_volume_number) = le_u8(i)?;
let (i, reserved3) = take(32_usize)(i)?;
let (i, maximum_number_of_track_sector_pairs) = le_u8(i)?;
let (i, reserved4) = take(8_usize)(i)?;
let (i, last_track_where_sectors_were_allocated) = le_u8(i)?;
let (i, direction_of_track_allocation) = le_i8(i)?;
let (i, reserved5) = take(2_usize)(i)?;
let (i, number_of_tracks_per_diskette) = le_u8(i)?;
let (i, number_of_sectors_per_track) = le_u8(i)?;
let (i, number_of_bytes_per_sector) = le_u16(i)?;
let bit_maps_to_read = min(number_of_tracks_per_diskette, 50);
let (i, bit_map_of_free_sectors) = count(take(4_usize), bit_maps_to_read.into())(i)?;
Ok((
i,
VolumeTableOfContents {
reserved,
track_number_of_first_catalog_sector,
sector_number_of_first_catalog_sector,
release_number_of_dos,
reserved2,
diskette_volume_number,
reserved3,
maximum_number_of_track_sector_pairs,
reserved4,
last_track_where_sectors_were_allocated,
direction_of_track_allocation,
reserved5,
number_of_tracks_per_diskette,
number_of_sectors_per_track,
number_of_bytes_per_sector,
bit_map_of_free_sectors,
},
))
}
impl SanityCheck for VolumeTableOfContents<'_> {
fn check(&self) -> bool {
if (self.number_of_tracks_per_diskette != 35) && (self.number_of_tracks_per_diskette != 40)
{
debug!(
"Suspicious number of tracks per diskette: {}",
self.number_of_tracks_per_diskette
);
return false;
}
if (self.number_of_sectors_per_track != 13) && (self.number_of_sectors_per_track != 16) {
debug!(
"Suspicious number of sectors per track: {}",
self.number_of_sectors_per_track
);
return false;
}
true
}
}
pub struct AppleDOSDisk<'a> {
pub volume_table_of_contents: VolumeTableOfContents<'a>,
pub catalog: FullCatalog<'a>,
pub tracks: Vec<Vec<&'a [u8]>>,
pub files: Files<'a>,
}
#[allow(clippy::large_enum_variant)]
pub enum AppleDiskData<'a> {
DOS(AppleDOSDisk<'a>),
ProDOS,
Nibble(NibbleDisk),
}
impl<'a> DiskImageSaver for AppleDOSDisk<'a> {
fn save_disk_image(
&self,
_config: &Config,
selected_filename: Option<&str>,
filename: &str,
) -> std::result::Result<(), crate::error::Error> {
if selected_filename.is_none() {
error!("Filename must be specified for saving Apple DOS 3.3 images");
return Err(crate::error::Error::new(ErrorKind::Message(String::from(
"Filename must be specified for saving Apple DOS 3.3 images",
))));
}
let selected_filename = selected_filename.unwrap();
let filename = PathBuf::from(filename);
let file_result = File::create(filename);
match file_result {
Ok(mut file) => {
let selected_file = self.files.get(selected_filename).unwrap();
file.write_all(&selected_file.data)?;
}
Err(e) => error!("Error opening file: {}", e),
}
Ok(())
}
}
pub struct AppleDisk<'a> {
pub encoding: Encoding,
pub format: Format,
pub data: AppleDiskData<'a>,
}
impl Display for AppleDisk<'_> {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "encoding: {}, format: {}", self.encoding, self.format)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AppleDiskGuess<'a> {
pub encoding: Encoding,
pub format: Format,
pub data: &'a [u8],
}
impl AppleDiskGuess<'_> {
pub fn new(encoding: Encoding, format: Format, data: &[u8]) -> AppleDiskGuess {
AppleDiskGuess {
encoding,
format,
data,
}
}
}
impl Display for AppleDiskGuess<'_> {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "encoding: {}, format: {}", self.encoding, self.format)
}
}
pub fn format_from_filename_and_data<'a>(
filename: &str,
data: &'a [u8],
) -> Option<AppleDiskGuess<'a>> {
let filename_extension: Vec<_> = filename.split('.').collect();
let path = Path::new(&filename);
let filesize = match fs::metadata(path) {
Ok(metadata) => metadata.len(),
Err(e) => {
error!("Couldn't get file metadata: {}", e);
panic!("Couldn't get file metadata");
}
};
match filename_extension[filename_extension.len() - 1]
.to_lowercase()
.as_str()
{
"dsk" => Some(AppleDiskGuess::new(
Encoding::Plain,
Format::DOS33(filesize),
data,
)),
"nib" => {
let prologue_byte_result = recognize_prologue(data);
let format = match prologue_byte_result {
Some(r) => match r {
0xB5 => Format::DOS32(filesize),
0x96 => Format::DOS33(filesize),
_ => Format::Unknown(filesize),
},
None => Format::Unknown(filesize),
};
Some(AppleDiskGuess::new(Encoding::Nibble, format, data))
}
&_ => None,
}
}
pub fn apple_tracks_parser(
track_size: usize,
number_of_tracks: usize,
) -> impl Fn(&[u8]) -> IResult<&[u8], Vec<&[u8]>> {
move |i| count(take(track_size), number_of_tracks)(i)
}
pub fn apple_140_k_dos_parser(
guess: AppleDiskGuess,
tracks_per_disk: usize,
) -> IResult<&[u8], Vec<&[u8]>> {
if tracks_per_disk == 35 {
apple_tracks_parser(4096, 35)(guess.data)
} else if tracks_per_disk == 40 {
apple_tracks_parser(3584, 40)(guess.data)
} else {
Err(Err::Error(nom::error::Error::new(
guess.data,
nom::error::ErrorKind::Fail,
)))
}
}
pub fn volume_parser(guess: AppleDiskGuess, filesize: u64) -> IResult<&[u8], AppleDisk> {
let tracks_per_disk = 35;
let catalog_sector_start = 17;
let (_i, raw_tracks) = apple_140_k_dos_parser(guess, tracks_per_disk)?;
let (i, vtoc) = parse_volume_table_of_contents(raw_tracks[catalog_sector_start])?;
debug!("VTOC: {}", vtoc);
if !vtoc.check() {
error!("Invalid data");
return Err(Err::Error(nom::error::Error::new(
i,
nom::error::ErrorKind::Fail,
)));
}
let mut tracks: Vec<Vec<&[u8]>> = Vec::new();
let catalog_sector = raw_tracks[catalog_sector_start][2];
for track in raw_tracks {
let mut track_vec: Vec<&[u8]> = Vec::new();
let (_i, sectors) = count(take(256_usize), 16)(track)?;
for sector in sectors {
track_vec.push(sector);
}
tracks.push(track_vec);
}
let catalog_res = parse_catalogs(
&tracks,
catalog_sector_start.try_into().unwrap(),
catalog_sector,
);
let catalog = match catalog_res {
Ok(catalog) => catalog,
Err(_e) => {
return Err(Err::Error(nom::error::Error::new(
i,
nom::error::ErrorKind::Fail,
)));
}
};
debug!("Catalog:\n{}", catalog);
let files = build_files(catalog.clone(), &tracks).unwrap();
let apple_dos_disk = AppleDOSDisk {
volume_table_of_contents: vtoc,
catalog,
tracks,
files,
};
Ok((
i,
AppleDisk {
encoding: Encoding::Plain,
format: Format::DOS33(filesize),
data: AppleDiskData::DOS(apple_dos_disk),
},
))
}
pub fn apple_disk_parser<'a, 'b>(
guess: AppleDiskGuess<'a>,
config: &'b Config,
) -> IResult<&'a [u8], AppleDisk<'a>> {
let i = guess.data;
debug!("Parsing based on guess: {}", guess);
match guess.encoding {
Encoding::Plain => {
let filesize = if let Format::DOS33(size) = guess.format {
size
} else {
0
};
if filesize == 143360 {
volume_parser(guess, filesize)
} else {
Err(Err::Error(nom::error::make_error(
i,
nom::error::ErrorKind::Fail,
)))
}
}
Encoding::Nibble => {
debug!("Parsing as nibble format");
let (i, disk) = parse_nib_disk(config)(i)?;
return Ok((
i,
AppleDisk {
encoding: guess.encoding,
format: guess.format,
data: AppleDiskData::Nibble(disk),
},
));
}
}
}
impl<'a, 'b> DiskImageParser<'a, 'b> for AppleDiskGuess<'a> {
fn parse_disk_image(
&'a self,
config: &'b Config,
_filename: &str,
) -> std::result::Result<DiskImage<'a>, Error> {
info!("DiskImageParser Attempting to parse Apple disk");
let result = apple_disk_parser(*self, config);
match result {
Ok(apple_disk) => Ok(DiskImage::Apple(apple_disk.1)),
Err(e) => Err(Error::new(ErrorKind::Invalid(InvalidErrorKind::Invalid(
nom::Err::Error(e).to_string(),
)))),
}
}
}
#[cfg(test)]
mod tests {
use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;
use config::Config;
use super::{
apple_disk_parser, format_from_filename_and_data, parse_volume_table_of_contents,
AppleDiskData, AppleDiskGuess, Encoding, Format,
};
const VTOC_DATA: [u8; 256] = [
0x00, 0x11, 0x0F, 0x03, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x12, 0x01, 0x00, 0x00, 0x23, 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00,
0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF,
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00,
0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF,
0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF,
0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00,
0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
];
#[test]
fn format_from_filename_works() {
let filename = "testdata/test-disk_format_from_filename_works.dsk";
let path = Path::new(&filename);
let mut file = OpenOptions::new()
.create(true)
.write(true)
.open(path)
.unwrap_or_else(|e| {
panic!("Couldn't open file: {}", e);
});
let data: [u8; 143360] = [0; 143360];
file.write_all(&data).unwrap_or_else(|e| {
panic!("Error writing test file: {}", e);
});
file.flush().unwrap_or_else(|e| {
panic!("Couldn't flush file stream: {}", e);
});
let guess = format_from_filename_and_data(filename, &data).unwrap_or_else(|| {
panic!("Invalid filename guess");
});
assert_eq!(
guess,
AppleDiskGuess::new(Encoding::Plain, Format::DOS33(143360), &data)
);
std::fs::remove_file(filename).unwrap_or_else(|e| {
panic!("Error removing test file: {}", e);
});
}
#[test]
fn parse_volume_table_of_contents_works() {
let vtoc_data = VTOC_DATA;
let result = parse_volume_table_of_contents(&vtoc_data);
match result {
Ok(vtoc) => {
assert_eq!(vtoc.1.track_number_of_first_catalog_sector, 17);
assert_eq!(vtoc.1.sector_number_of_first_catalog_sector, 15);
assert_eq!(vtoc.1.release_number_of_dos, 3);
assert_eq!(vtoc.1.diskette_volume_number, 254);
assert_eq!(vtoc.1.number_of_tracks_per_diskette, 35);
assert_eq!(vtoc.1.number_of_sectors_per_track, 16);
assert_eq!(vtoc.1.number_of_bytes_per_sector, 256);
assert_eq!(vtoc.1.last_track_where_sectors_were_allocated, 18);
}
Err(e) => {
panic!("Couldn't parse VTOC: {}", e);
}
}
}
#[test]
fn apple_disk_parser_disk_works() {
let filename = "testdata/test-apple_disk_parser_works.dsk";
let path = Path::new(&filename);
let mut data: Vec<u8> = Vec::new();
let data_prefix: [u8; 0x11000] = [0; 0x11000];
let data_vtoc = VTOC_DATA;
let data_suffix: [u8; 0x11F00] = [0; 0x11F00];
data.extend(data_prefix);
data.extend(data_vtoc);
data.extend(data_suffix);
std::fs::write(&path, &data).unwrap_or_else(|e| {
panic!("Error writing test file: {}", e);
});
let guess = AppleDiskGuess::new(Encoding::Plain, Format::DOS33(143360), &data);
let config = Config::default();
let res = apple_disk_parser(guess, &config);
match res {
Ok(disk) => match disk.1.data {
AppleDiskData::DOS(apple_dos_disk) => {
let vtoc = apple_dos_disk.volume_table_of_contents;
assert_eq!(disk.1.encoding, Encoding::Plain);
assert_eq!(disk.1.format, Format::DOS33(143360));
assert_eq!(vtoc.track_number_of_first_catalog_sector, 17);
assert_eq!(vtoc.sector_number_of_first_catalog_sector, 15);
assert_eq!(vtoc.release_number_of_dos, 3);
assert_eq!(vtoc.diskette_volume_number, 254);
assert_eq!(vtoc.number_of_tracks_per_diskette, 35);
assert_eq!(vtoc.number_of_sectors_per_track, 16);
assert_eq!(vtoc.number_of_bytes_per_sector, 256);
assert_eq!(vtoc.last_track_where_sectors_were_allocated, 18);
}
_ => {
panic!("Invalid format");
}
},
Err(_e) => {
panic!("This should have succeeded");
}
}
std::fs::remove_file(filename).unwrap_or_else(|e| {
panic!("Error removing test file: {}", e);
});
}
#[test]
fn apple_disk_parser_nonstandard_disk_panics() {
let filename = "testdata/test-apple_disk_parser_nonstandard_disk_panics.dsk";
let path = Path::new(&filename);
let data: [u8; 143360] = [0; 143360];
std::fs::write(&path, data).unwrap_or_else(|e| {
panic!("Error writing test file: {}", e);
});
let guess = AppleDiskGuess::new(Encoding::Plain, Format::DOS33(143360), &data);
let config = Config::default();
let res = apple_disk_parser(guess, &config);
match res {
Ok(_disk) => {
panic!("This should have failed parsing");
}
Err(_e) => {
assert_eq!(true, true);
}
}
std::fs::remove_file(filename).unwrap_or_else(|e| {
panic!("Error removing test file: {}", e);
});
}
}