use a2kit_macro::{DiskStructError,DiskStruct};
use a2kit_macro_derive::DiskStruct;
use crate::img;
use crate::img::{Track,Sector,SectorHood,meta};
use crate::img::tracks::{Method,FluxCells};
use crate::img::tracks::gcr::TrackEngine;
use crate::img::woz::{TMAP_ID,TRKS_ID,INFO_ID,META_ID};
use crate::bios::blocks::apple;
use crate::{STDRESULT,DYNERR,getByte,getByteEx,putByte,putStringBuf};
const TRACK_BYTE_CAPACITY: usize = 6646;
pub fn file_extensions() -> Vec<String> {
vec!["woz".to_string()]
}
#[derive(DiskStruct)]
pub struct Header {
vers: [u8;4],
high_bits: u8,
lfcrlf: [u8;3],
crc32: [u8;4]
}
#[derive(DiskStruct)]
pub struct Info {
id: [u8;4],
size: [u8;4],
vers: u8,
disk_type: u8,
write_protected: u8,
synchronized: u8,
cleaned: u8,
creator: [u8;32],
pad: [u8;23]
}
#[derive(DiskStruct)]
pub struct TMap {
id: [u8;4],
size: [u8;4],
map: [u8;160]
}
#[derive(DiskStruct,Clone,Copy)]
pub struct Trk {
bits: [u8;TRACK_BYTE_CAPACITY],
bytes_used: [u8;2],
bit_count: [u8;2],
splice_point: [u8;2],
splice_nib: u8,
splice_bit_count: u8,
pad: [u8;2]
}
pub struct Trks {
id: [u8;4],
size: [u8;4],
tracks: Vec<Trk>
}
pub struct Woz1 {
kind: img::DiskKind,
fmt: Option<img::tracks::DiskFormat>,
header: Header,
info: Info,
tmap: TMap,
trks: Trks,
meta: Option<Vec<u8>>,
engine: TrackEngine,
cells: Option<FluxCells>,
tmap_pos: usize,
}
impl Header {
fn create() -> Self {
Self {
vers: [0x57,0x4f,0x5a,0x31],
high_bits: 0xff,
lfcrlf: [0x0a,0x0d,0x0a],
crc32: [0,0,0,0]
}
}
}
impl Info {
fn create(kind: img::DiskKind) -> Self {
let creator_str = "a2kit v".to_string() + env!("CARGO_PKG_VERSION");
let mut creator: [u8;32] = [0x20;32];
for i in 0..creator_str.len() {
creator[i] = creator_str.as_bytes()[i];
}
Self {
id: u32::to_le_bytes(INFO_ID),
size: u32::to_le_bytes(60),
vers: 1,
disk_type: match kind {
img::DiskKind::D525(_) => 1,
_ => panic!("WOZ v1 can only accept physical 5.25 inch Apple formats")
},
write_protected: 0,
synchronized: 0,
cleaned: 0,
creator,
pad: [0;23]
}
}
fn verify_value(&self,key: &str,hex_str: &str) -> bool {
match key {
stringify!(disk_type) => hex_str=="01" || hex_str=="02",
stringify!(write_protected) => hex_str=="00" || hex_str=="01",
stringify!(synchronized) => hex_str=="00" || hex_str=="01",
stringify!(cleaned) => hex_str=="00" || hex_str=="01",
_ => true
}
}
}
impl TMap {
fn blank() -> Self {
let map: [u8;160] = [0xff;160];
Self {
id: u32::to_le_bytes(TMAP_ID),
size: u32::to_le_bytes(160),
map
}
}
fn create(fmt: &img::tracks::DiskFormat) -> Self {
let mut map: [u8;160] = [0xff;160];
let motor_head = fmt.get_motor_and_head();
let mut slot = 0;
for (m,h) in motor_head {
if h>0 {
panic!("WOZ v1 rejected side 2");
}
if m>0 {
map[m-1] = slot;
}
map[m] = slot;
if m<159 {
map[m+1] = slot;
}
slot += 1;
}
Self {
id: u32::to_le_bytes(TMAP_ID),
size: u32::to_le_bytes(160),
map
}
}
}
impl Trk {
fn create(hood: SectorHood,fmt: &img::tracks::ZoneFormat) -> Result<Self,DYNERR> {
fmt.check_flux_code(img::FluxCode::GCR)?;
let mut engine = TrackEngine::create(Method::Auto,false);
let cells = engine.format_track(hood, TRACK_BYTE_CAPACITY, fmt)?;
let (bits,_) = cells.to_woz_buf(Some(TRACK_BYTE_CAPACITY),0);
let bytes_used = u16::to_le_bytes(bits.len() as u16);
Ok(Self {
bits: bits.try_into().expect("track buffer mismatch"),
bytes_used,
bit_count: u16::to_le_bytes(cells.count() as u16),
splice_point: u16::to_le_bytes(0xffff),
splice_nib: 0,
splice_bit_count: 0,
pad: [0,0]
})
}
}
impl Trks {
fn blank() -> Self {
let mut ans = Trks::new();
ans.id = u32::to_le_bytes(TRKS_ID);
ans.size = [0,0,0,0];
return ans;
}
fn create(vol: u8,kind: &img::DiskKind,fmt: &img::tracks::DiskFormat,tmap: &[u8]) -> Result<Self,DYNERR> {
let mut ans = Trks::new();
ans.id = u32::to_le_bytes(TRKS_ID);
let mut end_slot = 0;
for slot in 0..160 {
if img::woz::get_trks_slot_id(vol,slot, tmap, None, kind).is_some() {
end_slot = slot + 1;
}
}
ans.size = u32::to_le_bytes(end_slot as u32 * Trk::new().len() as u32);
for slot in 0..end_slot {
if let Some((motor,hood,_)) = img::woz::get_trks_slot_id(vol, slot, tmap, None, kind) {
ans.tracks.push(Trk::create(hood,fmt.get_zone_fmt(motor as usize,0)?)?);
} else {
ans.tracks.push(Trk::new());
}
}
Ok(ans)
}
}
impl DiskStruct for Trks {
fn new() -> Self where Self: Sized {
Self {
id: [0,0,0,0],
size: [0,0,0,0],
tracks: Vec::new()
}
}
fn len(&self) -> usize {
8 + u32::from_le_bytes(self.size) as usize
}
fn update_from_bytes(&mut self,bytes: &[u8]) -> Result<(),DiskStructError> {
let sz = Trk::new().len();
self.id = [bytes[0],bytes[1],bytes[2],bytes[3]];
self.size = [bytes[4],bytes[5],bytes[6],bytes[7]];
let num_tracks = u32::from_le_bytes(self.size) as usize / sz;
let mut off = 8;
self.tracks = Vec::new();
for _track in 0..num_tracks {
let trk = Trk::from_bytes(&bytes[off..off+sz])?;
self.tracks.push(trk);
off += sz;
}
Ok(())
}
fn from_bytes(bytes: &[u8]) -> Result<Self,DiskStructError> where Self: Sized {
let mut ans = Trks::new();
ans.update_from_bytes(bytes)?;
Ok(ans)
}
fn to_bytes(&self) -> Vec<u8> {
let mut ans: Vec<u8> = Vec::new();
ans.append(&mut self.id.to_vec());
ans.append(&mut self.size.to_vec());
for trk in &self.tracks {
ans.append(&mut trk.to_bytes());
}
return ans;
}
}
impl Woz1 {
fn new() -> Self {
Self {
kind: img::DiskKind::Unknown,
fmt: None,
header: Header::new(),
info: Info::new(),
tmap: TMap::new(),
trks: Trks::new(),
meta: None,
engine: TrackEngine::create(Method::Auto,false),
cells: None,
tmap_pos: usize::MAX,
}
}
pub fn blank(kind: img::DiskKind) -> Self {
if kind!=img::names::A2_DOS32_KIND && kind!=img::names::A2_DOS33_KIND {
panic!("WOZ v1 can only accept 5.25 inch Apple formats")
}
Self {
kind,
fmt: None,
header: Header::create(),
info: Info::create(kind),
tmap: TMap::blank(),
trks: Trks::blank(),
meta: None,
engine: TrackEngine::create(Method::Auto,false),
cells: None,
tmap_pos: usize::MAX,
}
}
pub fn create(vol: u8,kind: img::DiskKind,maybe_fmt: Option<img::tracks::DiskFormat>) -> Result<Self,DYNERR> {
let fmt = match maybe_fmt {
Some(fmt) => fmt,
None => img::woz::kind_to_format(&kind).unwrap()
};
let tmap = TMap::create(&fmt);
let trks = Trks::create(vol,&kind,&fmt,&tmap.map)?;
Ok(Self {
kind,
fmt: Some(fmt),
header: Header::create(),
info: Info::create(kind),
tmap,
trks,
meta: None,
engine: TrackEngine::create(Method::Auto,false),
cells: None,
tmap_pos: usize::MAX,
})
}
fn sanity_check(&self) -> Result<(),DiskStructError> {
match u32::from_le_bytes(self.info.id)>0 && u32::from_le_bytes(self.tmap.id)>0 && u32::from_le_bytes(self.trks.id)>0 && self.info.disk_type==1 {
true => Ok(()),
false => {
log::debug!("WOZ v1 sanity checks failed");
return Err(DiskStructError::IllegalValue);
}
}
}
fn try_motor(&self,tmap_idx: usize) -> Result<usize,DYNERR> {
if self.tmap.map[tmap_idx] == 0xff {
log::info!("touched blank media at TMAP index {}",tmap_idx);
Err(Box::new(super::Error::BlankTrack))
} else if self.tmap.map[tmap_idx] as usize >= self.trks.tracks.len() {
Err(Box::new(super::Error::TrackNotFound))
} else {
Ok(self.tmap.map[tmap_idx] as usize)
}
}
fn get_trk_bits_ref(&self,trk: Track) -> Result<&[u8],DYNERR> {
let tmap_idx = img::woz::get_tmap_index(trk,&self.kind)?;
let idx = self.try_motor(tmap_idx)?;
return Ok(&self.trks.tracks[idx].bits);
}
fn get_trk_bits_mut(&mut self,trk: Track) -> Result<&mut [u8],DYNERR> {
let tmap_idx = img::woz::get_tmap_index(trk,&self.kind)?;
let idx = self.try_motor(tmap_idx)?;
return Ok(&mut self.trks.tracks[idx].bits);
}
fn write_back_track(&mut self) -> STDRESULT {
if let Some(cells) = &self.cells {
let idx = self.try_motor(self.tmap_pos)?;
let (buf,_) = cells.to_woz_buf(Some(TRACK_BYTE_CAPACITY),0);
self.trks.tracks[idx].bits = buf.try_into().expect("track buffer mismatch");
}
Ok(())
}
fn goto_track(&mut self,trk: Track) -> Result<[usize;3],DYNERR> {
let tmap_idx = img::woz::get_tmap_index(trk,&self.kind)?;
if self.tmap_pos != tmap_idx {
log::debug!("goto {} of {}",trk,self.kind);
self.write_back_track()?;
self.tmap_pos = tmap_idx;
match self.try_motor(tmap_idx) {
Ok(idx) => {
let bit_count = u16::from_le_bytes(self.trks.tracks[idx].bit_count) as usize;
let mut new_cells = FluxCells::from_woz_bits(bit_count, &self.trks.tracks[idx].bits,0,false);
if let Some(cells) = &self.cells {
new_cells.sync_to_other_track(cells);
}
self.cells = Some(new_cells);
},
Err(e) => match e.downcast_ref::<img::Error>() {
Some(img::Error::BlankTrack) => self.cells = None,
_ => return Err(e)
}
}
}
img::woz::get_motor_pos(trk, &self.kind)
}
}
impl img::DiskImage for Woz1 {
fn track_count(&self) -> usize {
let mut ans = 0;
for trk in &self.trks.tracks {
if trk.bit_count != [0;2] {
ans += 1;
}
}
ans
}
fn end_track(&self) -> usize {
let mut ans = 0;
for i in 0..160 {
if self.tmap.map[i] != 0xff {
ans = i;
}
}
ans / 4 + 1
}
fn num_heads(&self) -> usize {
1
}
fn motor_steps_per_cyl(&self) ->usize {
4
}
fn nominal_capacity(&self) -> Option<usize> {
Some(280*512)
}
fn actual_capacity(&mut self) -> Result<usize,DYNERR> {
let mut ans = 0;
let motor_stops = img::woz::find_motor_stops(&self.tmap.map, None);
for motor in motor_stops {
match self.get_track_solution(Track::Motor((motor,0))) {
Ok(img::TrackSolution::Solved(sol)) => ans += sol.size_map.iter().sum::<usize>(),
Ok(img::TrackSolution::Blank) => {},
Ok(img::TrackSolution::Unsolved) => return Err(Box::new(img::Error::UnknownFormat)),
Err(e) => return Err(e)
}
}
Ok(ans)
}
fn what_am_i(&self) -> img::DiskImageType {
img::DiskImageType::WOZ1
}
fn file_extensions(&self) -> Vec<String> {
file_extensions()
}
fn kind(&self) -> img::DiskKind {
self.kind
}
fn change_kind(&mut self,kind: img::DiskKind) {
self.kind = kind;
}
fn change_method(&mut self,method: img::tracks::Method) {
self.engine.change_method(method);
}
fn change_format(&mut self,fmt: img::tracks::DiskFormat) -> STDRESULT {
self.fmt = Some(fmt);
Ok(())
}
fn read_block(&mut self,addr: crate::bios::Block) -> Result<Vec<u8>,DYNERR> {
apple::read_block(self, addr)
}
fn write_block(&mut self, addr: crate::bios::Block, dat: &[u8]) -> STDRESULT {
apple::write_block(self, addr, dat)
}
fn read_sector(&mut self,trk: Track,sec: Sector) -> Result<Vec<u8>,DYNERR> {
let [motor,head,_] = self.goto_track(trk)?;
if self.cells.is_none() {
return Err(Box::new(img::Error::BlankTrack));
}
if self.fmt.is_none() {
return Err(Box::new(img::Error::UnknownDiskKind));
}
let fmt = self.fmt.as_ref().unwrap(); let zfmt = fmt.get_zone_fmt(motor,head)?;
let hood = SectorHood::a2_525(254, u8::try_from((motor+1)/4)?);
let ans = self.engine.read_sector(self.cells.as_mut().unwrap(),&hood,&sec,zfmt)?;
Ok(ans)
}
fn write_sector(&mut self,trk: Track,sec: Sector,dat: &[u8]) -> Result<(),DYNERR> {
let [motor,head,_] = self.goto_track(trk)?;
if self.cells.is_none() {
return Err(Box::new(img::Error::BlankTrack));
}
if self.fmt.is_none() {
return Err(Box::new(img::Error::UnknownDiskKind));
}
let fmt = self.fmt.as_ref().unwrap(); let zfmt = fmt.get_zone_fmt(motor,head)?.clone();
let hood = SectorHood::a2_525(254, u8::try_from((motor+1)/4)?);
self.engine.write_sector(self.cells.as_mut().unwrap(),dat,&hood,&sec,&zfmt)?;
Ok(())
}
fn from_bytes(buf: &[u8]) -> Result<Self,DiskStructError> where Self: Sized {
if buf.len()<12 {
return Err(DiskStructError::UnexpectedSize);
}
let mut ans = Woz1::new();
ans.header.update_from_bytes(&buf[0..12].to_vec())?;
if ans.header.vers!=[0x57,0x4f,0x5a,0x31] {
return Err(DiskStructError::IllegalValue);
}
log::info!("identified WOZ v1 header");
let mut ptr: usize= 12;
while ptr>0 {
let (next,id,maybe_chunk) = img::woz::get_next_chunk(ptr, buf);
match (id,maybe_chunk) {
(INFO_ID,Some(chunk)) => ans.info.update_from_bytes(&chunk)?,
(TMAP_ID,Some(chunk)) => ans.tmap.update_from_bytes(&chunk)?,
(TRKS_ID,Some(chunk)) => ans.trks.update_from_bytes(&chunk)?,
(META_ID,Some(chunk)) => ans.meta = Some(chunk),
_ => if id!=0 {
log::info!("unprocessed chunk with id {:08X}/{}",id,String::from_utf8_lossy(&u32::to_le_bytes(id)))
}
}
ptr = next;
}
ans.kind = img::names::A2_DOS33_KIND;
ans.sanity_check()?;
for baseline_motor_stop in [0,12,2] {
for baseline_method in [Method::Fast,Method::Emulate] {
log::info!("baseline scan of track {}, method {}",baseline_motor_stop as f32/4.0,baseline_method);
ans.change_method(baseline_method);
match ans.get_track_solution(Track::Motor((baseline_motor_stop,0))) {
Ok(img::TrackSolution::Solved(_)) => {
log::info!("baseline solution is {}",ans.kind);
return Ok(ans);
},
Ok(img::TrackSolution::Unsolved) => log::debug!("could not solve"),
Ok(img::TrackSolution::Blank) => log::debug!("blank track"),
Err(e) => log::warn!("{}",e)
}
}
}
log::warn!("no baseline, continuing with {}",ans.kind);
ans.fmt = img::woz::kind_to_format(&ans.kind);
return Ok(ans);
}
fn to_bytes(&mut self) -> Vec<u8> {
self.write_back_track().expect("could not restore track");
let mut ans: Vec<u8> = Vec::new();
ans.append(&mut self.header.to_bytes());
ans.append(&mut self.info.to_bytes());
ans.append(&mut self.tmap.to_bytes());
ans.append(&mut self.trks.to_bytes());
if let Some(mut meta) = self.meta.clone() {
ans.append(&mut meta);
}
let crc = u32::to_le_bytes(img::woz::crc32(0, &ans[12..].to_vec()));
ans[8] = crc[0];
ans[9] = crc[1];
ans[10] = crc[2];
ans[11] = crc[3];
return ans;
}
fn get_track_buf(&mut self,trk: Track) -> Result<Vec<u8>,DYNERR> {
Ok(self.get_trk_bits_ref(trk)?.to_vec())
}
fn set_track_buf(&mut self,trk: Track,dat: &[u8]) -> STDRESULT {
let bits = self.get_trk_bits_mut(trk)?;
if bits.len()!=dat.len() {
log::error!("source track buffer is {} bytes, destination track buffer is {} bytes",dat.len(),bits.len());
return Err(Box::new(img::Error::ImageSizeMismatch));
}
bits.copy_from_slice(dat);
Ok(())
}
fn get_track_solution(&mut self,trk: Track) -> Result<img::TrackSolution,DYNERR> {
let [motor,head,_] = self.goto_track(trk)?;
if self.cells.is_none() {
return Ok(img::TrackSolution::Blank);
}
if let Some(fmt) = &self.fmt {
log::trace!("try current format");
let zfmt = fmt.get_zone_fmt(motor,head)?;
if let Ok((addr_map,size_map)) = self.engine.get_sector_map(self.cells.as_mut().unwrap(),zfmt) {
return Ok(zfmt.track_solution(addr_map,size_map,"VTSK",[255,255,255,255,0,0],None));
}
}
log::trace!("try standard DOS 13-sector");
self.kind = img::names::A2_DOS32_KIND;
self.fmt = img::woz::kind_to_format(&self.kind);
let zfmt = img::tracks::get_zone_fmt(motor,head,&self.fmt)?;
if let Ok((addr_map,size_map)) = self.engine.get_sector_map(self.cells.as_mut().unwrap(),zfmt) {
if addr_map.len()==13 {
return Ok(zfmt.track_solution(addr_map,size_map,"VTSK",[255,255,255,255,0,0],None));
}
}
log::trace!("try standard DOS 16-sector");
self.kind = img::names::A2_DOS33_KIND;
self.fmt = img::woz::kind_to_format(&self.kind);
let zfmt = img::tracks::get_zone_fmt(motor,head,&self.fmt)?;
if let Ok((addr_map,size_map)) = self.engine.get_sector_map(self.cells.as_mut().unwrap(),zfmt) {
if addr_map.len()==16 {
return Ok(zfmt.track_solution(addr_map,size_map,"VTSK",[255,255,255,255,0,0],None));
}
}
return Ok(img::TrackSolution::Unsolved);
}
fn export_geometry(&mut self,indent: Option<u16>) -> Result<String,DYNERR> {
let pkg = img::package_string(&self.kind());
let mut track_sols = Vec::new();
let motor_stops = img::woz::find_motor_stops(&self.tmap.map, None);
for motor in motor_stops {
match self.get_track_solution(Track::Motor((motor,0))) {
Ok(sol) => track_sols.push((motor as f64 / 4.0, 0 as usize, sol)),
Err(e) => log::warn!("{}",e)
}
}
img::geometry_json(pkg,track_sols,35,1,4,indent)
}
fn export_format(&self,indent: Option<u16>) -> Result<String,DYNERR> {
match &self.fmt {
Some(f) => f.to_json(indent),
None => Err(Box::new(super::Error::UnknownFormat))
}
}
fn get_track_nibbles(&mut self,trk: Track) -> Result<Vec<u8>,DYNERR> {
let [motor,head,_] = self.goto_track(trk)?;
if self.cells.is_none() {
return Err(Box::new(img::Error::BlankTrack));
}
let zfmt = img::tracks::get_zone_fmt(motor, head, &self.fmt)?;
Ok(self.engine.to_nibbles(self.cells.as_mut().unwrap(), zfmt))
}
fn display_track(&self,bytes: &[u8]) -> String {
let trk = super::woz::trk_from_tmap_idx(self.tmap_pos, &self.kind);
let [motor,head,_] = img::woz::get_motor_pos(trk, &self.kind).expect("could not get head position");
let zfmt = match img::tracks::get_zone_fmt(motor, head, &self.fmt) {
Ok(z) => Some(z),
_ => None
};
super::woz::track_string_for_display(0, &bytes, zfmt)
}
fn get_metadata(&self,indent: Option<u16>) -> String {
let mut root = json::JsonValue::new_object();
let woz1 = self.what_am_i().to_string();
root[&woz1] = json::JsonValue::new_object();
root[&woz1]["info"] = json::JsonValue::new_object();
getByteEx!(root,woz1,self.info.disk_type);
root[&woz1]["info"]["disk_type"]["_pretty"] = json::JsonValue::String(match self.info.disk_type {
1 => "Apple 5.25 inch".to_string(),
2 => "Apple 3.5 inch".to_string(),
_ => "Unexpected value".to_string()
});
getByte!(root,woz1,self.info.write_protected);
getByte!(root,woz1,self.info.synchronized);
getByte!(root,woz1,self.info.cleaned);
root[woz1]["info"]["creator"] = json::JsonValue::String(String::from_utf8_lossy(&self.info.creator).trim_end().to_string());
if let Some(spaces) = indent {
json::stringify_pretty(root,spaces)
} else {
json::stringify(root)
}
}
fn put_metadata(&mut self,key_path: &Vec<String>,maybe_str_val: &json::JsonValue) -> STDRESULT {
if let Some(val) = maybe_str_val.as_str() {
log::debug!("put key `{:?}` with val `{}`",key_path,val);
let woz1 = self.what_am_i().to_string();
meta::test_metadata(key_path, self.what_am_i())?;
if meta::match_key(key_path,&[&woz1,"info","disk_type"]) {
log::warn!("skipping read-only `disk_type`");
return Ok(());
}
if key_path.len()>2 && key_path[0]=="woz1" && key_path[1]=="info" {
if !self.info.verify_value(&key_path[2], val) {
log::error!("INFO chunk key `{}` had a bad value `{}`",key_path[2],val);
return Err(Box::new(img::Error::MetadataMismatch));
}
}
putByte!(val,key_path,woz1,self.info.write_protected);
putByte!(val,key_path,woz1,self.info.synchronized);
putByte!(val,key_path,woz1,self.info.cleaned);
putStringBuf!(val,key_path,woz1,self.info.creator,0x20);
}
log::error!("unresolved key path {:?}",key_path);
Err(Box::new(img::Error::MetadataMismatch))
}
}