a2kit/img/
mod.rs

1//! # Disk Image Module
2//! 
3//! Disk images are represented by objects implementing the `DiskImage` trait.
4//! The object type is usually named for the disk image type that it handles, e.g., `Woz2`.
5//! This object is perhaps best thought of as a disk plus all the hardware and some of the
6//! low level software that runs it.
7//! 
8//! ## Basic Functions
9//! 
10//! The trait includes reading and writing tracks, sectors, and blocks.
11//! It is agnostic as to the specific manner in which tracks are represented.
12//! Creating and formatting disks is left to specific implementations.
13//! An important design element is that a disk image can refuse a request as out of scope.
14//! As an example, PO images will only handle ProDOS blocks, since the original disk
15//! geometry cannot be known (and may not even exist, although in other environments
16//! it is appropriate to guess one).
17//! 
18//! ## Relation to File Systems
19//! 
20//! The `DiskImage` trait object serves as the underlying storage for `fs` modules.
21//! The `fs` modules work by reading blocks from, or writing blocks to, the disk image.
22//! The task of mapping blocks to sectors happens in submodules of `img`, sometimes with
23//! the aid of `bios`, but never with any help from `fs`.
24//! The `fs` module will usually run heuristics on certain key blocks when a disk
25//! image is first connected.  If these fail the disk image is refused.
26//! 
27//! ## Disk Kind Patterns
28//! 
29//! There is an enumeration called `DiskKind` that can be used to create a disk image.
30//! The `names` submodule contains convenient constants that can be passed to creation functions.
31//! You can also use this to make a `match` selection based on parameters of a given disk.
32//! As an example, if you want to do something only with 3.5 inch disks, you would use a pattern like
33//! `DiskKind::D35(_)`.  The embedded structure can be used to limit the pattern to specific elements
34//! of a track layout.
35//! 
36//! ## Sector Addresses
37//! 
38//! Assuming soft-sectoring, once we have a track, the only way to locate a sector is by matching its
39//! address field.  The unique part of an address field is often a sector id that is just an unsigned
40//! 8-bit integer, with other bytes encoding cylinder numbers, volume numbers, etc..  The address that
41//! is finally used in a sector search is the product of multiple transformations.  If there is a block
42//! request, the standard file system transformation is applied first.  This may involve a skew
43//! transformation.  Then if there is a special format, a further transformation is applied.  Finally
44//! the actual pattern is matched against the track bits.  In the case of naive sector images,
45//! special format transformations can become ineffective.
46//! 
47//! ## Sector Skews
48//! 
49//! There are two kinds of sector skews.  The first kind of skew is a physical skew,
50//! wherein geometric neighbors have disjoint addresses.  The second kind is a logical skew,
51//! wherein geometric neighbors have neighboring addresses, but neighboring blocks
52//! are mapped to disjoint sectors. Tables describing these orderings are in `bios::skew`.
53//! These tables are used by `img` in a variety of ways due to the variety of ways that
54//! disk images organize sectors.
55
56pub mod dsk_d13;
57pub mod dsk_do;
58pub mod dsk_po;
59pub mod dsk_img;
60pub mod dot2mg;
61pub mod nib;
62pub mod woz;
63pub mod woz1;
64pub mod woz2;
65pub mod imd;
66pub mod td0;
67pub mod names;
68pub mod meta;
69pub mod tracks;
70
71use std::str::FromStr;
72use std::fmt;
73use log::{info,error};
74use crate::bios::Block;
75use crate::{STDRESULT,DYNERR};
76use tracks::DiskFormat;
77
78use a2kit_macro::DiskStructError;
79
80/// Enumerates disk image errors.  The `Display` trait will print equivalent long message.
81#[derive(thiserror::Error,Debug)]
82pub enum Error {
83    #[error("unknown kind of disk")]
84    UnknownDiskKind,
85    #[error("unknown image type")]
86    UnknownImageType,
87    #[error("unknown format")]
88    UnknownFormat,
89    #[error("invalid kind of disk")]
90    DiskKindMismatch,
91    #[error("geometric coordinate out of range")]
92    GeometryMismatch,
93	#[error("image size did not match the request")]
94	ImageSizeMismatch,
95    #[error("image type not compatible with request")]
96    ImageTypeMismatch,
97    #[error("error while accessing internal structures")]
98    InternalStructureAccess,
99    #[error("unable to access sector")]
100    SectorAccess,
101    #[error("sector not found")]
102    SectorNotFound,
103    #[error("unable to access track")]
104    TrackAccess,
105    #[error("track request out of range")]
106    TrackNotFound,
107    #[error("track is unformatted")]
108    BlankTrack,
109    #[error("metadata mismatch")]
110    MetadataMismatch,
111    #[error("wrong context for this request")]
112    BadContext,
113    #[error("invalid byte while decoding")]
114    InvalidByte,
115    #[error("bad checksum found in a sector")]
116    BadChecksum,
117    #[error("could not find bit pattern")]
118    BitPatternNotFound,
119    #[error("nibble type appeared in wrong context")]
120    NibbleType,
121    #[error("track lies outside expected zones")]
122    UnexpectedZone
123}
124
125/// Encapsulates 3 ways a track might be idenfified
126#[derive(Clone,Copy,PartialEq)]
127pub enum Track {
128    /// single index to a track, often `C * num_heads + H`
129    Num(usize),
130    /// cylinder and head
131    CH((usize, usize)),
132    /// stepper motor position and head, needed for, e.g., WOZ quarter tracks
133    Motor((usize, usize)),
134}
135
136/// Properties of a sector's neighborhood that may be used in forming its address.
137/// Format objects determine how these values map to a specific address.
138pub struct SectorHood {
139    vol: u8,
140    cyl: u8,
141    head: u8,
142    aux: u8
143}
144
145/// Wraps either a standard sector index or an explicit address.
146/// If the index is used, it will in general be combined with `ZoneFormat` and `SectorHood` to produce
147/// the actual sector address.  If the explicit address is used, there are no transformations.
148#[derive(Clone,PartialEq)]
149pub enum Sector {
150    /// standard index used by a file system, subject to various transformations
151    Num(usize),
152    /// (index,address), the address is the explicit sector address to seek without transformation,
153    /// the index may be used to determined other sector properties in the usual way 
154    Addr((usize,Vec<u8>))
155}
156
157/// Indicates the overall scheme of a track
158#[derive(PartialEq,Eq,Clone,Copy)]
159pub enum FluxCode {
160    None,
161    FM,
162    GCR,
163    MFM
164}
165
166/// Indicates the encoding of a disk field, this is
167/// only necessary for GCR tracks (evidently), for
168/// others set to None.
169#[derive(PartialEq,Eq,Clone,Copy)]
170pub enum FieldCode {
171    None,
172    WOZ((usize,usize)),
173    G64((usize,usize)),
174    IBM((usize,usize))
175}
176
177#[derive(PartialEq,Eq,Clone,Copy)]
178pub struct BlockLayout {
179    block_size: usize,
180    block_count: usize
181}
182
183/// Detailed layout of a single track, this will normally be deduced from actual track data
184/// in response to a caller's request for a track solution.
185pub struct SolvedTrack {
186    flux_code: FluxCode,
187    addr_code: FieldCode,
188    data_code: FieldCode,
189    /// nominal rate of pulses during a run of high bits, n.b. this is not the same as the data rate, e.g. for FM clock pulses are counted
190    speed_kbps: usize,
191    /// Measures the relative angular density of data, e.g., for a disk spinning at the nominal rate the maximum pulse rate
192    /// is `speed_kbps * density`.  Should only be Some if the track supplies timing information.
193    density: Option<f64>,
194    /// string describing address (like VTS or CHSF)
195    addr_type: String,
196    /// mask out bits we are ignorant of due to image limitations
197    addr_mask: [u8;6],
198    /// address of every sector
199    addr_map: Vec<[u8;6]>,
200    size_map: Vec<usize>
201}
202
203/// We can have a track known to be blank, a track that seems to have data but
204/// could not be solved, or a full track solution.  If there is a solution
205/// the discriminant wraps a `SolvedTrack`.
206pub enum TrackSolution {
207    Blank,
208    Unsolved,
209    Solved(SolvedTrack)
210}
211
212/// Fixed size representation of how all the tracks on a disk are layed out,
213/// useful for pattern matching.  The simplifying assumptions are
214/// * at most 5 zones on the disk
215/// * every track in a zone is laid out the same
216/// * every sector on a track is laid out the same
217#[derive(PartialEq,Eq,Clone,Copy)]
218pub struct TrackLayout {
219    cylinders: [usize;5],
220    sides: [usize;5],
221    sectors: [usize;5],
222    sector_size: [usize;5],
223    flux_code: [FluxCode;5],
224    addr_code: [FieldCode;5],
225    data_code: [FieldCode;5],
226    speed_kbps: [usize;5]
227}
228
229/// This enumeration is often used in a match arm to take different
230/// actions depending on the kind of disk.  It is in the form
231/// package(layout), where the layout is a fixed size representation
232/// of the track details that can be efficiently pattern matched.
233#[derive(PartialEq,Eq,Clone,Copy)]
234pub enum DiskKind {
235    Unknown,
236    LogicalBlocks(BlockLayout),
237    LogicalSectors(TrackLayout),
238    D3(TrackLayout),
239    D35(TrackLayout),
240    D525(TrackLayout),
241    D8(TrackLayout)
242}
243
244#[derive(PartialEq,Clone,Copy)]
245pub enum DiskImageType {
246    D13,
247    DO,
248    PO,
249    IMG,
250    WOZ1,
251    WOZ2,
252    IMD,
253    DOT2MG,
254    NIB,
255    TD0,
256    /// for future expansion
257    DOT86F,
258    /// for future expansion
259    D64,
260    /// for future expansion
261    G64,
262    /// for future expansion
263    MFI,
264    /// for future expansion
265    MFM,
266    /// for future expansion
267    HFE,
268}
269
270impl TrackLayout {
271    pub fn track_count(&self) -> usize {
272        let mut ans = 0;
273        for i in 0..5 {
274            ans += self.cylinders[i] * self.sides[i];
275        }
276        ans
277    }
278    pub fn sides(&self) -> usize {
279        *self.sides.iter().max().unwrap()
280    }
281    pub fn zones(&self) -> usize {
282        for i in 0..5 {
283            if self.cylinders[i]==0 {
284                return i;
285            }
286        }
287        5
288    }
289    pub fn zone(&self,track_num: usize) -> usize {
290        let mut tcount: [usize;5] = [0;5];
291        tcount[0] = self.cylinders[0] * self.sides[0];
292        for i in 1..5 {
293            tcount[i] = tcount[i-1] + self.cylinders[i] * self.sides[i];
294        }
295        match track_num {
296            n if n < tcount[0] => 0,
297            n if n < tcount[1] => 1,
298            n if n < tcount[2] => 2,
299            n if n < tcount[3] => 3,
300            _ => 4
301        }
302    }
303    pub fn byte_capacity(&self) -> usize {
304        let mut ans = 0;
305        for i in 0..5 {
306            ans += self.cylinders[i] * self.sides[i] * self.sectors[i] * self.sector_size[i];
307        }
308        ans
309    }
310    // fn sector_bytes(&self,track: usize) -> usize {
311    //     let zone = self.zone(track);
312    //     self.sector_size[zone]
313    // }
314}
315
316/// Allows the track layout to be displayed to the console using `println!`.  This also
317/// derives `to_string`, so the enum can be converted to `String`.
318impl fmt::Display for TrackLayout {
319    fn fmt(&self,f: &mut fmt::Formatter<'_>) -> fmt::Result {
320        let mut cyls = 0;
321        for c in self.cylinders {
322            cyls += c;
323        }
324        write!(f,"{}/{}/{}/{}",cyls,self.sides(),self.sectors[0],self.sector_size[0])
325    }
326}
327
328/// Allows the disk kind to be displayed to the console using `println!`.  This also
329/// derives `to_string`, so the enum can be converted to `String`.
330impl fmt::Display for DiskKind {
331    fn fmt(&self,f: &mut fmt::Formatter<'_>) -> fmt::Result {
332        match *self {
333            DiskKind::LogicalBlocks(lay) => write!(f,"Logical disk, {} blocks",lay.block_count),
334            DiskKind::LogicalSectors(lay) => write!(f,"Logical disk, {}",lay),
335            names::A2_400_KIND => write!(f,"Apple 3.5 inch 400K"),
336            names::A2_800_KIND => write!(f,"Apple 3.5 inch 800K"),
337            names::A2_DOS32_KIND => write!(f,"Apple 5.25 inch 13 sector"),
338            names::A2_DOS33_KIND => write!(f,"Apple 5.25 inch 16 sector"),
339            DiskKind::D3(lay) => write!(f,"3.0 inch {}",lay),
340            DiskKind::D35(lay) => write!(f,"3.5 inch {}",lay),
341            DiskKind::D525(lay) => write!(f,"5.25 inch {}",lay),
342            DiskKind::D8(lay) => write!(f,"8 inch {}",lay),
343            DiskKind::Unknown => write!(f,"unknown")
344        }
345    }
346}
347
348impl fmt::Display for FieldCode {
349    fn fmt(&self,f: &mut fmt::Formatter<'_>) -> fmt::Result {
350        match *self {
351            FieldCode::WOZ((x,y)) => write!(f,"{}&{}",x,y),
352            FieldCode::G64((x,y)) => write!(f,"G64-{}:{}",x,y),
353            FieldCode::IBM((x,y)) => write!(f,"IBM-{}:{}",x,y),
354            FieldCode::None => write!(f,"none")
355        }
356    }
357}
358
359impl FromStr for FieldCode {
360    type Err = Error;
361    fn from_str(s: &str) -> Result<Self,Self::Err> {
362        match s {
363            "4&4" => Ok(FieldCode::WOZ((4,4))),
364            "5&3" => Ok(FieldCode::WOZ((5,3))),
365            "6&2" => Ok(FieldCode::WOZ((6,2))),
366            "G64-5:4" => Ok(FieldCode::G64((5,4))),
367            "IBM-5:4" => Ok(FieldCode::IBM((5,4))),
368            "none" => Ok(FieldCode::None),
369            _ => Err(Error::MetadataMismatch)
370        }
371    }
372}
373
374impl FromStr for FluxCode {
375    type Err = Error;
376    fn from_str(s: &str) -> Result<Self,Self::Err> {
377        match s {
378            "FM" => Ok(FluxCode::FM),
379            "MFM" => Ok(FluxCode::MFM),
380            "GCR" => Ok(FluxCode::GCR),
381            "none" => Ok(FluxCode::None),
382            _ => Err(Error::MetadataMismatch)
383        }
384    }
385}
386
387impl fmt::Display for FluxCode {
388    fn fmt(&self,f: &mut fmt::Formatter<'_>) -> fmt::Result {
389        match *self {
390            FluxCode::FM => write!(f,"FM"),
391            FluxCode::MFM => write!(f,"MFM"),
392            FluxCode::GCR => write!(f,"GCR"),
393            FluxCode::None => write!(f,"none")
394        }
395    }
396}
397
398/// match command line argument to disk kind
399impl FromStr for DiskKind {
400    type Err = Error;
401    fn from_str(s: &str) -> Result<Self,Self::Err> {
402        match s {
403            "8in-ibm-sssd" => Ok(names::IBM_CPM1_KIND),
404            "8in-trs80-ssdd" => Ok(names::TRS80_M2_CPM_KIND),
405            "8in-nabu-dsdd" => Ok(names::NABU_CPM_KIND),
406            "5.25in-ibm-ssdd8" => Ok(Self::D525(names::IBM_SSDD_8)),
407            "5.25in-ibm-ssdd9" => Ok(Self::D525(names::IBM_SSDD_9)),
408            "5.25in-ibm-dsdd8" => Ok(Self::D525(names::IBM_DSDD_8)),
409            "5.25in-ibm-dsdd9" => Ok(Self::D525(names::IBM_DSDD_9)),
410            "5.25in-ibm-ssqd" => Ok(Self::D525(names::IBM_SSQD)),
411            "5.25in-ibm-dsqd" => Ok(Self::D525(names::IBM_DSQD)),
412            "5.25in-ibm-dshd" => Ok(Self::D525(names::IBM_DSHD)),
413            "5.25in-osb-sssd" => Ok(names::OSBORNE1_SD_KIND),
414            "5.25in-osb-ssdd" => Ok(names::OSBORNE1_DD_KIND),
415            "5.25in-kay-ssdd" => Ok(names::KAYPROII_KIND),
416            "5.25in-kay-dsdd" => Ok(names::KAYPRO4_KIND),
417            "5.25in-apple-13" => Ok(names::A2_DOS32_KIND),
418            "5.25in-apple-16" => Ok(names::A2_DOS33_KIND),
419            "3.5in-apple-400" => Ok(names::A2_400_KIND),
420            "3.5in-apple-800" => Ok(names::A2_800_KIND),
421            "3.5in-ibm-720" => Ok(Self::D35(names::IBM_720)),
422            "3.5in-ibm-1440" => Ok(Self::D35(names::IBM_1440)),
423            "3.5in-ibm-2880" => Ok(Self::D35(names::IBM_2880)),
424            "3in-amstrad-ssdd" => Ok(names::AMSTRAD_SS_KIND),
425            "hdmax" => Ok(names::A2_HD_MAX),
426            _ => Err(Error::UnknownDiskKind)
427        }
428    }
429}
430
431impl FromStr for DiskImageType {
432    type Err = Error;
433    fn from_str(s: &str) -> Result<Self,Self::Err> {
434        match s {
435            "d13" => Ok(Self::D13),
436            "do" => Ok(Self::DO),
437            "po" => Ok(Self::PO),
438            "img" => Ok(Self::IMG),
439            "woz1" => Ok(Self::WOZ1),
440            "woz2" => Ok(Self::WOZ2),
441            "imd" => Ok(Self::IMD),
442            "2mg" => Ok(Self::DOT2MG),
443            "2img" => Ok(Self::DOT2MG),
444            "nib" => Ok(Self::NIB),
445            "td0" => Ok(Self::TD0),
446            _ => Err(Error::UnknownImageType)
447        }
448    }
449}
450
451impl fmt::Display for DiskImageType {
452    fn fmt(&self,f: &mut fmt::Formatter<'_>) -> fmt::Result {
453        match self {
454            Self::D13 => write!(f,"d13"),
455            Self::DO => write!(f,"do"),
456            Self::PO => write!(f,"po"),
457            Self::IMG => write!(f,"img"),
458            Self::WOZ1 => write!(f,"woz1"),
459            Self::WOZ2 => write!(f,"woz2"),
460            Self::IMD => write!(f,"imd"),
461            Self::DOT2MG => write!(f,"2mg"),
462            Self::NIB => write!(f,"nib"),
463            Self::TD0 => write!(f,"td0"),
464            Self::D64 => write!(f,"d64"),
465            Self::DOT86F => write!(f,"86f"),
466            Self::G64 => write!(f,"g64"),
467            Self::HFE => write!(f,"hfe"),
468            Self::MFM => write!(f,"mfm"),
469            Self::MFI => write!(f,"mfi")
470        }
471    }
472}
473
474/// The main trait for working with any kind of disk image.
475/// The corresponding trait object serves as storage for `DiskFS`.
476/// Reading can mutate the object because the image may be keeping
477/// track of the head position or other status indicators.
478pub trait DiskImage {
479    /// Get the count of formatted tracks, not necessarily the same as `end_track`
480    fn track_count(&self) -> usize;
481    /// Get the id of the end-track (last-track + 1)
482    fn end_track(&self) -> usize;
483    fn num_heads(&self) -> usize;
484    fn motor_steps_per_cyl(&self) ->usize {
485        1
486    }
487    /// Get the geometric [cyl,head].  Default truncates fractional tracks in a reasonable
488    /// way if there are either 1 or 4 steps per track.
489    fn get_rz(&self,trk: Track) -> Result<[usize;2],DYNERR> {
490        let msc = self.motor_steps_per_cyl();
491        let ans = match trk {
492            Track::Num(t) => [t/self.num_heads(),t%self.num_heads()],
493            Track::CH((c,h)) => [c,h],
494            Track::Motor((m,h)) => [(m+msc/4)/msc,h]
495        };
496        Ok(ans)
497    }
498    /// Get the geometric track.  Default truncates fractional tracks in a reasonable
499    /// way if there are either 1 or 4 steps per track.
500    fn get_track(&self,trk: Track) -> Result<usize,DYNERR> {
501        let msc = self.motor_steps_per_cyl();
502        let ans = match trk {
503            Track::Num(t) => t,
504            Track::CH((c,h)) => c*self.num_heads() + h,
505            Track::Motor((m,h)) => ((m+msc/4)/msc)*self.num_heads() + h
506        };
507        Ok(ans)
508    }
509    /// Get the geometric [cyl,head,sec].
510    /// Default truncates fractional tracks in a reasonable way if there are either 1 or 4 steps per track.
511    /// If an explicit address is given, the sector will be taken from the most likely address byte.
512    fn get_rzq(&self,trk: Track,sec: Sector) -> Result<[usize;3],DYNERR> {
513        let [c,h] = self.get_rz(trk)?;
514        let s = match sec {
515            Sector::Num(s) => s,
516            Sector::Addr((_,addr)) => {
517                match self.kind() {
518                    names::A2_400_KIND | names::A2_800_KIND => addr[1] as usize,
519                    _ => addr[2] as usize
520                }
521            }
522        };
523        Ok([c,h,s])
524    }
525    /// Get the capacity in bytes supposing this disk were formatted in a standard way.
526    /// May return `None` if format hints are insufficient.
527    fn nominal_capacity(&self) -> Option<usize>;
528    /// Get the capacity in bytes given the way the disk is actually formatted.
529    /// The expense can be high, and may change the disk state.
530    fn actual_capacity(&mut self) -> Result<usize,DYNERR>;
531    fn what_am_i(&self) -> DiskImageType;
532    fn file_extensions(&self) -> Vec<String>;
533    fn kind(&self) -> DiskKind;
534    /// Change the kind of disk, but do not change the format
535    fn change_kind(&mut self,kind: DiskKind);
536    /// Change details of how sectors are identified and decoded
537    fn change_format(&mut self,_fmt: DiskFormat) -> STDRESULT {
538        Err(Box::new(Error::ImageTypeMismatch))
539    }
540    /// Change the broad method by which nibbles are extracted from a track.
541    /// `Emulate` will try to produce nibbles just as the hardware would.
542    /// `Fast` and `Analyze` will show something more idealized.
543    fn change_method(&mut self,_method: tracks::Method) {
544    }
545    fn from_bytes(buf: &[u8]) -> Result<Self,DiskStructError> where Self: Sized;
546    fn to_bytes(&mut self) -> Vec<u8>;
547    /// Read a block from the image; can affect disk state
548    fn read_block(&mut self,addr: Block) -> Result<Vec<u8>,DYNERR>;
549    /// Write a block to the image
550    fn write_block(&mut self, addr: Block, dat: &[u8]) -> STDRESULT;
551    /// Read a physical sector from the image; can affect disk state.
552    fn read_sector(&mut self,trk: Track,sec: Sector) -> Result<Vec<u8>,DYNERR>;
553    /// Write a physical sector to the image
554    fn write_sector(&mut self,trk: Track,sec: Sector,dat: &[u8]) -> STDRESULT;
555    /// Get the track buffer exactly in the form the image stores it
556    fn get_track_buf(&mut self,trk: Track) -> Result<Vec<u8>,DYNERR>;
557    /// Set the track buffer using another track buffer, the sizes must match
558    fn set_track_buf(&mut self,trk: Track,dat: &[u8]) -> STDRESULT;
559    /// Determined sector layout and update internal formatting hints.
560    /// Implement this at a low level, making as few assumptions as possible.
561    /// The expense of this operation can vary widely depending on the image type.
562    fn get_track_solution(&mut self,trk: Track) -> Result<TrackSolution,DYNERR>;
563    /// Get the track bytes as aligned nibbles; for user inspection
564    fn get_track_nibbles(&mut self,trk: Track) -> Result<Vec<u8>,DYNERR>;
565    /// Write the track to a string suitable for display, input should be pre-aligned nibbles, e.g. from `get_track_nibbles`.
566    /// Any required details of the track format have to come from the internal state of the image.
567    fn display_track(&self,bytes: &[u8]) -> String;
568    /// Get image metadata into JSON string.
569    /// Default contains only the image type.
570    fn get_metadata(&self,indent: Option<u16>) -> String {
571        let mut root = json::JsonValue::new_object();
572        let typ = self.what_am_i().to_string();
573        root[typ] = json::JsonValue::new_object();
574        if let Some(spaces) = indent {
575            json::stringify_pretty(root,spaces)
576        } else {
577            json::stringify(root)
578        }
579    }
580    /// Add or change a single metadata item.  This is designed to take as its arguments the
581    /// outputs produced by walking a JSON tree with `crate::JsonCursor`.
582    /// The `key_path` has the keys leading up to the leaf, e.g., `/type/item/_raw`, and
583    /// the `val` is the JSON value associated with the leaf (anything but an object).
584    /// The special keys `_raw` and `_pretty` should be handled as follows.
585    /// If a leaf is neither `_raw` nor `_pretty` treat it as raw.
586    /// If a leaf is `_pretty` ignore it.
587    fn put_metadata(&mut self,key_path: &Vec<String>, _val: &json::JsonValue) -> STDRESULT {
588        meta::test_metadata(key_path,self.what_am_i())
589    }
590    /// Write the disk geometry, including all track solutions, into a JSON string
591    fn export_geometry(&mut self,indent: Option<u16>) -> Result<String,DYNERR> {
592        let pkg = package_string(&self.kind());
593        let mut track_sols = Vec::new();
594        for trk in 0..self.end_track() {
595            log::trace!("solve track {}",trk);
596            let sol = self.get_track_solution(Track::Num(trk))?;
597            let [c,h] = self.get_rz(Track::Num(trk))?;
598            track_sols.push((c as f64,h,sol));
599        }
600        geometry_json(pkg,track_sols,self.end_track(),self.num_heads(),self.motor_steps_per_cyl(),indent)
601    }
602    /// Write the abstract disk format into a JSON string
603    fn export_format(&self,_indent: Option<u16>) -> Result<String,DYNERR> {
604        Err(Box::new(Error::UnknownFormat))
605    }
606}
607
608fn solved_track_json(sol: SolvedTrack) -> Result<json::JsonValue,DYNERR> {
609    let mut ans = json::JsonValue::new_object();
610    ans["flux_code"] = match sol.flux_code {
611        FluxCode::None => json::JsonValue::Null,
612        f => json::JsonValue::String(f.to_string())
613    };
614    ans["addr_code"] = match sol.addr_code {
615        FieldCode::None => json::JsonValue::Null,
616        n => json::JsonValue::String(n.to_string())
617    };
618    ans["nibble_code"] = match sol.data_code {
619        FieldCode::None => json::JsonValue::Null,
620        n => json::JsonValue::String(n.to_string())
621    };
622    ans["speed_kbps"] = json::JsonValue::Number(sol.speed_kbps.into());
623    ans["density"] = match sol.density {
624        Some(val) => json::JsonValue::Number(val.into()),
625        None => json::JsonValue::Null
626    };
627    ans["addr_map"] = json::JsonValue::new_array();
628    for addr in sol.addr_map {
629        ans["addr_map"].push(json::JsonValue::String(hex::encode_upper(&addr[0..sol.addr_type.len()])))?;
630    }
631    ans["size_map"] = json::JsonValue::new_array();
632    for size in sol.size_map {
633        ans["size_map"].push(size)?;
634    }
635    ans["addr_type"] = json::JsonValue::String(sol.addr_type);
636    ans["addr_mask"] = json::JsonValue::new_array();
637    for by in sol.addr_mask {
638        ans["addr_mask"].push(by)?;
639    }
640    Ok(ans)
641}
642
643/// Create geometry string for external consumption, `cylinders` should be the nominal cylinder count for this
644/// kind of disk, the actuals will be computed and provided automatically.
645fn geometry_json(pkg: String,desc: Vec<(f64,usize,TrackSolution)>,cylinders: usize,heads: usize,width: usize,indent: Option<u16>) -> Result<String,DYNERR> {
646    let mut root = json::JsonValue::new_object();
647    root["package"] = json::JsonValue::String(pkg);
648    let mut trk_ary = json::JsonValue::new_array();
649    let mut blank_track_count = 0;
650    let mut solved_track_count = 0;
651    let mut unsolved_track_count = 0;
652    let mut last_blank_track: Option<usize> = None;
653    let mut last_solved_track: Option<usize> = None;
654    let mut last_unsolved_track: Option<usize> = None;
655    let mut idx = 0;
656    for (fcyl,head,sol) in desc {
657        let mut trk_obj = json::JsonValue::new_object();
658        trk_obj["cylinder"] = json::JsonValue::Number(fcyl.into());
659        trk_obj["head"] = json::JsonValue::Number(head.into());
660        let ignore = match sol {
661            TrackSolution::Blank => {
662                if fcyl < cylinders as f64 {
663                    blank_track_count += 1;
664                    last_blank_track = Some(idx);
665                }
666                fcyl >= cylinders as f64
667            },
668            TrackSolution::Unsolved => {
669                unsolved_track_count += 1;
670                last_unsolved_track = Some(idx);
671                false
672            },
673            TrackSolution::Solved(_) => {
674                solved_track_count += 1;
675                last_solved_track = Some(idx);
676                false
677            }
678        };
679        trk_obj["solution"] = match sol {
680            TrackSolution::Blank => json::JsonValue::String("blank".to_string()),
681            TrackSolution::Unsolved => json::JsonValue::String("unsolved".to_string()),
682            TrackSolution::Solved(sol) => solved_track_json(sol)?
683        };
684        if !ignore {
685            trk_ary.push(trk_obj)?;
686        }
687        idx += 1;
688    }
689
690    root["summary"] = json::JsonValue::new_object();
691    root["summary"]["cylinders"] = json::JsonValue::Number(cylinders.into());
692    root["summary"]["heads"] = json::JsonValue::Number(heads.into());
693    root["summary"]["blank_tracks"] = json::JsonValue::Number(blank_track_count.into());
694    root["summary"]["solved_tracks"] = json::JsonValue::Number(solved_track_count.into());
695    root["summary"]["unsolved_tracks"] = json::JsonValue::Number(unsolved_track_count.into());
696    root["summary"]["last_blank_track"] = match last_blank_track {
697        Some(t) => json::JsonValue::Number(t.into()),
698        None => json::JsonValue::Null
699    };
700    root["summary"]["last_solved_track"] = match last_solved_track {
701        Some(t) => json::JsonValue::Number(t.into()),
702        None => json::JsonValue::Null
703    };
704    root["summary"]["last_unsolved_track"] = match last_unsolved_track {
705        Some(t) => json::JsonValue::Number(t.into()),
706        None => json::JsonValue::Null
707    };
708    root["summary"]["steps_per_cyl"] = json::JsonValue::Number(width.into());
709
710    if trk_ary.len()==0 {
711        root["tracks"] = json::JsonValue::Null;
712    } else {
713        root["tracks"] = trk_ary;
714    }
715    if let Some(spaces) = indent {
716        Ok(json::stringify_pretty(root,spaces))
717    } else {
718        Ok(json::stringify(root))
719    }
720}
721
722/// Test a buffer for a size match to DOS-oriented track and sector counts.
723pub fn is_dos_size(dsk: &Vec<u8>,allowed_track_counts: &Vec<usize>,sectors: usize) -> STDRESULT {
724    let bytes = dsk.len();
725    for tracks in allowed_track_counts {
726        if bytes==tracks*sectors*256 {
727            return Ok(());
728        }
729    }
730    info!("image size was {}",bytes);
731    return Err(Box::new(Error::ImageSizeMismatch));
732}
733
734/// If a data source is smaller than `quantum` bytes, pad it with zeros.
735/// If it is larger, do not include the extra bytes.
736pub fn quantize_block(src: &[u8],quantum: usize) -> Vec<u8> {
737	let mut padded: Vec<u8> = Vec::new();
738	for i in 0..quantum {
739		if i<src.len() {
740			padded.push(src[i])
741		} else {
742			padded.push(0);
743		}
744	}
745    return padded;
746}
747
748/// Package designation for geometry JSON (e.g., "3.5", "5.25", ...)
749pub fn package_string(kind: &DiskKind) -> String {
750    match kind {
751        DiskKind::D3(_) => "3".to_string(),
752        DiskKind::D35(_) => "3.5".to_string(),
753        DiskKind::D525(_) => "5.25".to_string(),
754        DiskKind::D8(_) => "8".to_string(),
755        DiskKind::LogicalBlocks(_) => "logical".to_string(),
756        DiskKind::LogicalSectors(_) => "logical".to_string(),
757        DiskKind::Unknown => "unknown".to_string()
758    }
759}
760
761fn highest_bit(mut val: usize) -> u8 {
762    let mut ans = 0;
763    while val > 0 {
764        ans += 1;
765        val = val >> 1;
766    }
767    ans
768}
769
770/// Calculate the IBM CRC bytes given the sector address and optionally custom sync bytes and IDAM.
771/// The full address including CRC is returned.
772pub fn append_ibm_crc(addr: [u8;4],maybe_sync: Option<[u8;4]>) -> [u8;6]
773{
774    let mut buf = vec![];
775    match maybe_sync {
776        Some(sync) => buf.append(&mut sync.to_vec()),
777        None => buf.append(&mut vec![0xa1,0xa1,0xa1,0xfe])
778    };
779    buf.append(&mut addr.to_vec());
780    let buf = [[0xa1,0xa1,0xa1,0xfe],[addr[0],addr[1],addr[2],addr[3]]].concat();
781    let mut crc: u16 = 0xffff;
782    for i in 0..buf.len() {
783        crc ^= (buf[i] as u16) << 8;
784        for _bit in 0..8 {
785            crc = (crc << 1) ^ match crc & 0x8000 { 0 => 0, _ => 0x1021 };
786        }
787    }
788    let be = u16::to_be_bytes(crc);
789    [addr[0],addr[1],addr[2],addr[3],be[0],be[1]]
790}