diff --git a/README.md b/README.md index e8393d4..b0b3dd4 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Supported floppy image formats: * Applesauce A2R 2.x and 3.x (flux) * Applesauce MOOF (bitstream and flux) * PCE Flux Image (PFI, flux) + * PCE Raw Image (PRI, bitstream) * Raw images (sector-based) ## Building and running from source diff --git a/floppy/src/loaders/auto.rs b/floppy/src/loaders/auto.rs index 35d0010..875aca9 100644 --- a/floppy/src/loaders/auto.rs +++ b/floppy/src/loaders/auto.rs @@ -1,16 +1,13 @@ //! Auto-detect image file type and load -use crate::loaders::A2Rv2; use crate::{ - loaders::{Bitfile, Diskcopy42, FloppyImageLoader, Moof, RawImage}, + loaders::{A2Rv2, A2Rv3, Bitfile, Diskcopy42, FloppyImageLoader, Moof, RawImage, PFI, PRI}, FloppyImage, FloppyType, }; use anyhow::{bail, Result}; use strum::{Display, IntoEnumIterator}; -use super::{A2Rv3, PFI}; - /// Types of supported floppy images #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Display, Copy, Clone)] pub enum ImageType { @@ -20,6 +17,7 @@ pub enum ImageType { Bitfile, DC42, PFI, + PRI, Raw, } @@ -32,6 +30,7 @@ impl ImageType { Self::Bitfile => "Bitfile", Self::DC42 => "Apple DiskCopy 4.2", Self::PFI => "PCE PFI", + Self::PRI => "PCE PRI", Self::Raw => "Raw image", } } @@ -57,6 +56,10 @@ impl Autodetect { if data.len() >= 4 && data[0..4] == *b"PFI " { return Ok(ImageType::PFI); } + // PFI + if data.len() >= 4 && data[0..4] == *b"PRI " { + return Ok(ImageType::PRI); + } // Bitfile / 'Dave format' if data.len() >= 10 && data[0..4] == data[4..8] @@ -87,6 +90,7 @@ impl FloppyImageLoader for Autodetect { ImageType::Bitfile => Bitfile::load(data, filename), ImageType::DC42 => Diskcopy42::load(data, filename), ImageType::PFI => PFI::load(data, filename), + ImageType::PRI => PRI::load(data, filename), ImageType::Raw => RawImage::load(data, filename), } } diff --git a/floppy/src/loaders/mod.rs b/floppy/src/loaders/mod.rs index 804f6f5..0ce389b 100644 --- a/floppy/src/loaders/mod.rs +++ b/floppy/src/loaders/mod.rs @@ -5,6 +5,7 @@ mod bitfile; mod diskcopy42; mod moof; mod pfi; +mod pri; mod raw; use std::path::Path; @@ -17,6 +18,7 @@ pub use bitfile::Bitfile; pub use diskcopy42::Diskcopy42; pub use moof::Moof; pub use pfi::PFI; +pub use pri::PRI; pub use raw::RawImage; use crate::FloppyImage; diff --git a/floppy/src/loaders/pri.rs b/floppy/src/loaders/pri.rs new file mode 100644 index 0000000..c878b26 --- /dev/null +++ b/floppy/src/loaders/pri.rs @@ -0,0 +1,183 @@ +//! PCE PRI format +//! Bitstream format +//! http://www.hampa.ch/pce/index.html + +use std::collections::HashMap; +use std::io::{Seek, SeekFrom}; + +use super::FloppyImageLoader; +use crate::{Floppy, FloppyImage, FloppyType, OriginalTrackType, TrackLength}; + +use anyhow::{bail, Result}; +use binrw::io::Cursor; +use binrw::{binrw, BinRead}; +use log::*; + +const CRC_PRI: crc::Algorithm = crc::Algorithm { + width: 32, + poly: 0x1edc6f41, + init: 0, + refin: false, + refout: false, + xorout: 0, + check: 0, + residue: 0, +}; + +/// Standardized chunk header +#[binrw] +#[brw(big)] +#[derive(Debug)] +struct ChunkHeader { + /// ASCII chunk identifier + pub id: [u8; 4], + + /// Chunk size in bytes + pub size: u32, +} + +/// Standardized chunk tail +#[binrw] +#[brw(big)] +#[derive(Debug)] +struct ChunkTail { + pub crc: u32, +} + +/// File header chunk payload +#[binrw] +#[brw(big)] +#[derive(Debug)] +struct PayloadHeader { + pub version: u8, +} + +/// Track header +#[binrw] +#[brw(big)] +#[derive(Debug)] +struct PayloadTrack { + pub track: u32, + pub head: u32, + pub length: u32, + pub clock: u32, +} + +/// PCE PRI image format loader +pub struct PRI {} + +impl FloppyImageLoader for PRI { + fn load(data: &[u8], filename: Option<&str>) -> Result { + let mut cursor = Cursor::new(data); + + let mut tracks: HashMap<(usize, usize), (usize, &[u8])> = HashMap::new(); + let mut cur_track = 0; + let mut cur_side = 0; + let mut cur_len = 0; + let mut saw_header = false; + + // Parse chunks from file + while let Ok(chunk) = ChunkHeader::read(&mut cursor) { + let startpos = cursor.position(); + + // Check CRC of the entire chunk + let checksum = crc::Crc::::new(&CRC_PRI).checksum( + &data[(startpos as usize - 8)..(startpos as usize + chunk.size as usize)], + ); + let chunk_checksum = u32::from_be_bytes( + data[(startpos as usize + chunk.size as usize) + ..(startpos as usize + chunk.size as usize + 4)] + .try_into()?, + ); + if checksum != chunk_checksum { + bail!( + "Checksum for chunk '{}' incorrect, saw {:08X}, expected {:08X}", + String::from_utf8_lossy(&chunk.id), + chunk_checksum, + checksum + ); + } + + if &chunk.id != b"PRI " && !saw_header { + bail!("File header not found"); + } + + match &chunk.id { + b"PRI " => { + saw_header = true; + } + b"TRAK" => { + let payload = PayloadTrack::read(&mut cursor)?; + + if payload.clock != 500_000 { + bail!( + "Unsupported clock rate {} on side {} track {}", + payload.clock, + cur_side, + cur_track + ); + } + cur_track = payload.track as usize; + cur_side = payload.head as usize; + cur_len = payload.length as usize; + } + b"TEXT" => (), + b"DATA" => { + tracks.insert( + (cur_side, cur_track), + ( + cur_len, + &data[(startpos as usize + 8) + ..(startpos as usize + chunk.size as usize)], + ), + ); + } + b"BCLK" => { + bail!( + "BCLK encountered on side {}, track {}. Currently unsupported", + cur_side, + cur_track + ); + } + b"END " => break, + _ => { + warn!( + "Found unsupported chunk '{}', skipping", + String::from_utf8_lossy(&chunk.id) + ); + } + } + + // Always consume the amount of bytes the chunk header reports + cursor.seek(SeekFrom::Start(startpos + u64::from(chunk.size) + 4))?; + } + + let mut img = FloppyImage::new_empty( + if tracks.keys().any(|&(s, _t)| s > 0) { + FloppyType::Mac800K + } else { + FloppyType::Mac400K + }, + filename.unwrap_or_default(), + ); + + // Fill tracks + for ((side, track), (bitlen, data)) in tracks { + if let TrackLength::Bits(t) = img.get_track_length(side, track) { + if t > 0 { + // Multiple captures encountered, we just use the first and hope it's good. + continue; + } + } + img.origtracktype[side][track] = OriginalTrackType::Bitstream; + img.set_actual_track_length(side, track, bitlen); + + // The DATA chunk may be shorter than the track length in the + // preceding TRAK chunk suggests. In that case the remainder of + // the track data should be set to 0. + img.trackdata[side][track][0..data.len()].copy_from_slice(data); + } + + Ok(img) + } +}