chdlady-disc 0.2.2

Disc geometries and physical media formats for chdlady
//! Hard disk geometry (CHS) definitions, templates, and metadata parsing.
use crate::error::DiscError;

/// Hard disk physical geometry (Cylinders, Heads, Sectors, Bytes per Sector).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HardDiskGeo {
    /// Total number of cylinders.
    pub cylinders: u32,
    /// Total number of heads.
    pub heads: u32,
    /// Sectors per track.
    pub sectors: u32,
    /// Sector payload size in bytes (typically 512).
    pub bytes_per_sector: u32,
}

impl HardDiskGeo {
    /// Creates a new hard disk geometry definition.
    pub fn new(cylinders: u32, heads: u32, sectors: u32, bytes_per_sector: u32) -> Self {
        Self {
            cylinders,
            heads,
            sectors,
            bytes_per_sector,
        }
    }

    /// Returns the total sector count (`cylinders * heads * sectors`).
    pub fn total_sectors(&self) -> u64 {
        self.cylinders as u64 * self.heads as u64 * self.sectors as u64
    }

    /// Returns the total logical byte capacity of the disk image.
    pub fn total_bytes(&self) -> u64 {
        self.total_sectors() * self.bytes_per_sector as u64
    }

    /// Formats the geometry as a CHD `GDDD` metadata string: `"CYLS:%d,HEADS:%d,SECS:%d,BPS:%d"`.
    pub fn to_metadata_string(&self) -> String {
        format!(
            "CYLS:{},HEADS:{},SECS:{},BPS:{}",
            self.cylinders, self.heads, self.sectors, self.bytes_per_sector
        )
    }

    /// Parses a CHD `GDDD` metadata string.
    pub fn parse_metadata(s: &str) -> Result<Self, DiscError> {
        let mut cyls = None;
        let mut heads = None;
        let mut secs = None;
        let mut bps = None;

        let trimmed = s.trim().trim_matches('\0');
        for part in trimmed.split([',', ' ']) {
            let part = part.trim();
            if part.is_empty() {
                continue;
            }
            if let Some((k, v)) = part.split_once(':') {
                match k.trim() {
                    "CYLS" => {
                        cyls = Some(v.trim().parse::<u32>().map_err(|_| {
                            DiscError::InvalidMetadata(format!("invalid CYLS in '{}'", s))
                        })?);
                    }
                    "HEADS" => {
                        heads = Some(v.trim().parse::<u32>().map_err(|_| {
                            DiscError::InvalidMetadata(format!("invalid HEADS in '{}'", s))
                        })?);
                    }
                    "SECS" => {
                        secs = Some(v.trim().parse::<u32>().map_err(|_| {
                            DiscError::InvalidMetadata(format!("invalid SECS in '{}'", s))
                        })?);
                    }
                    "BPS" => {
                        bps = Some(v.trim().parse::<u32>().map_err(|_| {
                            DiscError::InvalidMetadata(format!("invalid BPS in '{}'", s))
                        })?);
                    }
                    _ => {}
                }
            }
        }

        match (cyls, heads, secs, bps) {
            (Some(c), Some(h), Some(s), Some(b)) => Ok(Self::new(c, h, s, b)),
            _ => Err(DiscError::InvalidMetadata(format!(
                "incomplete hard disk geometry metadata: '{}'",
                s
            ))),
        }
    }
}

/// Standard hard disk template definition matching MAME `hd_template`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HdTemplate {
    /// Drive manufacturer name.
    pub manufacturer: &'static str,
    /// Model name or number.
    pub model: &'static str,
    /// Cylinders.
    pub cylinders: u32,
    /// Heads.
    pub heads: u32,
    /// Sectors per track.
    pub sectors: u32,
    /// Sector size in bytes.
    pub sector_size: u32,
}

/// Standard predefined hard disk templates.
pub const HD_TEMPLATES: &[HdTemplate] = &[
    HdTemplate {
        manufacturer: "Conner",
        model: "CFA170A",
        cylinders: 332,
        heads: 16,
        sectors: 63,
        sector_size: 512,
    },
    HdTemplate {
        manufacturer: "Rodime",
        model: "R0201",
        cylinders: 321,
        heads: 2,
        sectors: 16,
        sector_size: 512,
    },
    HdTemplate {
        manufacturer: "Rodime",
        model: "R0202",
        cylinders: 321,
        heads: 4,
        sectors: 16,
        sector_size: 512,
    },
    HdTemplate {
        manufacturer: "Rodime",
        model: "R0203",
        cylinders: 321,
        heads: 6,
        sectors: 16,
        sector_size: 512,
    },
    HdTemplate {
        manufacturer: "Rodime",
        model: "R0204",
        cylinders: 321,
        heads: 8,
        sectors: 16,
        sector_size: 512,
    },
    HdTemplate {
        manufacturer: "Seagate",
        model: "ST-213",
        cylinders: 615,
        heads: 2,
        sectors: 17,
        sector_size: 512,
    },
    HdTemplate {
        manufacturer: "Seagate",
        model: "ST-225",
        cylinders: 615,
        heads: 4,
        sectors: 17,
        sector_size: 512,
    },
    HdTemplate {
        manufacturer: "Seagate",
        model: "ST-251",
        cylinders: 820,
        heads: 6,
        sectors: 17,
        sector_size: 512,
    },
    HdTemplate {
        manufacturer: "Seagate",
        model: "ST-3600N",
        cylinders: 1877,
        heads: 7,
        sectors: 76,
        sector_size: 512,
    },
    HdTemplate {
        manufacturer: "Maxtor",
        model: "LXT-213S",
        cylinders: 1314,
        heads: 7,
        sectors: 53,
        sector_size: 512,
    },
    HdTemplate {
        manufacturer: "Maxtor",
        model: "LXT-340S",
        cylinders: 1574,
        heads: 7,
        sectors: 70,
        sector_size: 512,
    },
    HdTemplate {
        manufacturer: "Maxtor",
        model: "MXT-540SL",
        cylinders: 2466,
        heads: 7,
        sectors: 87,
        sector_size: 512,
    },
    HdTemplate {
        manufacturer: "Micropolis",
        model: "1528",
        cylinders: 2094,
        heads: 15,
        sectors: 83,
        sector_size: 512,
    },
    HdTemplate {
        manufacturer: "Quantum",
        model: "Fireball CR 4.3 AT",
        cylinders: 14848,
        heads: 9,
        sectors: 63,
        sector_size: 512,
    },
    HdTemplate {
        manufacturer: "Quantum",
        model: "Fireball CR 6.4 AT",
        cylinders: 13328,
        heads: 15,
        sectors: 63,
        sector_size: 512,
    },
    HdTemplate {
        manufacturer: "Quantum",
        model: "Fireball CR 8.4 AT",
        cylinders: 16383,
        heads: 16,
        sectors: 63,
        sector_size: 512,
    },
    HdTemplate {
        manufacturer: "Quantum",
        model: "Fireball CR 13.0 AT",
        cylinders: 25228,
        heads: 16,
        sectors: 63,
        sector_size: 512,
    },
];

/// Guesses hard disk CHS geometry from byte length and sector size using MAME's search heuristic.
pub fn guess_chs(file_size: u64, sector_size: u32) -> Result<HardDiskGeo, DiscError> {
    if file_size == 0 || sector_size == 0 {
        return Err(DiscError::InvalidMetadata(
            "cannot guess CHS for empty file".into(),
        ));
    }
    let mut total_sectors = (file_size / sector_size as u64) as u32;
    loop {
        for cur_sectors in (2..=63).rev() {
            if total_sectors.is_multiple_of(cur_sectors) {
                let total_heads = total_sectors / cur_sectors;
                for cur_heads in (2..=16).rev() {
                    if total_heads.is_multiple_of(cur_heads) {
                        let cylinders = total_heads / cur_heads;
                        return Ok(HardDiskGeo::new(
                            cylinders,
                            cur_heads,
                            cur_sectors,
                            sector_size,
                        ));
                    }
                }
            }
        }
        total_sectors = total_sectors.checked_add(1).ok_or_else(|| {
            DiscError::InvalidMetadata("overflow searching for CHS geometry".into())
        })?;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_hard_disk_geo_roundtrip() {
        let geo = HardDiskGeo::new(1024, 16, 63, 512);
        let s = geo.to_metadata_string();
        assert_eq!(s, "CYLS:1024,HEADS:16,SECS:63,BPS:512");
        let parsed = HardDiskGeo::parse_metadata(&s).expect("parse metadata");
        assert_eq!(geo, parsed);
    }

    #[test]
    fn test_guess_chs_64mb() {
        let geo = guess_chs(67_108_864, 512).expect("guess chs 64mb");
        assert_eq!(geo.cylinders, 256);
        assert_eq!(geo.heads, 16);
        assert_eq!(geo.sectors, 32);
        assert_eq!(geo.bytes_per_sector, 512);
    }
}