#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DiskGeometry {
pub cylinders: u32,
pub heads: u32,
pub sectors: u32,
}
impl DiskGeometry {
#[must_use]
pub fn chs_sectors(&self) -> u64 {
u64::from(self.cylinders) * u64::from(self.heads) * u64::from(self.sectors)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DiskDatabase {
pub adapter_type: Option<String>,
pub geometry: Option<DiskGeometry>,
pub bios_geometry: Option<DiskGeometry>,
pub virtual_hw_version: Option<String>,
pub tools_version: Option<String>,
pub uuid: Option<String>,
pub long_content_id: Option<String>,
pub thin_provisioned: Option<bool>,
pub encoding: Option<String>,
pub entries: Vec<(String, String)>,
}
impl DiskDatabase {
#[must_use]
pub fn parse(descriptor_text: &str) -> Self {
let mut db = DiskDatabase::default();
let (mut cyl, mut head, mut sect) = (None, None, None);
let (mut bcyl, mut bhead, mut bsect) = (None, None, None);
for line in descriptor_text.lines() {
let line = line.trim();
let Some(rest) = line.strip_prefix("ddb.") else {
continue;
};
let Some((key, value)) = rest.split_once('=') else {
continue;
};
let key = key.trim();
let value = value.trim().trim_matches('"').to_owned();
let full_key = format!("ddb.{key}");
db.entries.push((full_key, value.clone()));
match key {
"adapterType" => db.adapter_type = Some(value),
"virtualHWVersion" => db.virtual_hw_version = Some(value),
"toolsVersion" => db.tools_version = Some(value),
"uuid" => db.uuid = Some(value),
"longContentID" => db.long_content_id = Some(value),
"encoding" => db.encoding = Some(value),
"thinProvisioned" => db.thin_provisioned = Some(value.trim() == "1"),
"geometry.cylinders" => cyl = value.parse().ok(),
"geometry.heads" => head = value.parse().ok(),
"geometry.sectors" => sect = value.parse().ok(),
"geometry.biosCylinders" => bcyl = value.parse().ok(),
"geometry.biosHeads" => bhead = value.parse().ok(),
"geometry.biosSectors" => bsect = value.parse().ok(),
_ => {}
}
}
if let (Some(cylinders), Some(heads), Some(sectors)) = (cyl, head, sect) {
db.geometry = Some(DiskGeometry {
cylinders,
heads,
sectors,
});
}
if let (Some(cylinders), Some(heads), Some(sectors)) = (bcyl, bhead, bsect) {
db.bios_geometry = Some(DiskGeometry {
cylinders,
heads,
sectors,
});
}
db
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&str> {
self.entries
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
const FULL: &str = "# Disk DescriptorFile\nversion=1\nCID=12345678\nparentCID=ffffffff\ncreateType=\"monolithicSparse\"\n\nddb.adapterType = \"lsilogic\"\nddb.geometry.cylinders = \"16383\"\nddb.geometry.heads = \"16\"\nddb.geometry.sectors = \"63\"\nddb.virtualHWVersion = \"7\"\nddb.toolsVersion = \"10338\"\nddb.uuid = \"60 00 C2 97 1a 2b 3c 4d-5e 6f 70 81 92 a3 b4 c5\"\nddb.longContentID = \"deadbeefcafef00d1122334455667788\"\nddb.thinProvisioned = \"1\"\nddb.encoding = \"UTF-8\"\n";
#[test]
fn parses_adapter_type_and_versions() {
let db = DiskDatabase::parse(FULL);
assert_eq!(db.adapter_type.as_deref(), Some("lsilogic"));
assert_eq!(db.virtual_hw_version.as_deref(), Some("7"));
assert_eq!(db.tools_version.as_deref(), Some("10338"));
assert_eq!(db.encoding.as_deref(), Some("UTF-8"));
}
#[test]
fn parses_geometry() {
let db = DiskDatabase::parse(FULL);
let g = db.geometry.expect("geometry present");
assert_eq!(g.cylinders, 16383);
assert_eq!(g.heads, 16);
assert_eq!(g.sectors, 63);
assert_eq!(g.chs_sectors(), 16383 * 16 * 63);
}
#[test]
fn parses_uuid_thin_and_long_content_id() {
let db = DiskDatabase::parse(FULL);
assert_eq!(
db.uuid.as_deref(),
Some("60 00 C2 97 1a 2b 3c 4d-5e 6f 70 81 92 a3 b4 c5")
);
assert_eq!(
db.long_content_id.as_deref(),
Some("deadbeefcafef00d1122334455667788")
);
assert_eq!(db.thin_provisioned, Some(true));
}
#[test]
fn empty_when_no_ddb_section() {
let db = DiskDatabase::parse(
"# Disk DescriptorFile\nversion=1\ncreateType=\"monolithicFlat\"\n",
);
assert!(db.is_empty());
assert_eq!(db.adapter_type, None);
assert_eq!(db.geometry, None);
assert_eq!(db.thin_provisioned, None);
}
#[test]
fn unknown_ddb_keys_are_retained() {
let db = DiskDatabase::parse("ddb.somethingNew = \"42\"\nddb.adapterType = \"ide\"\n");
assert_eq!(db.adapter_type.as_deref(), Some("ide"));
assert_eq!(db.get("ddb.somethingNew"), Some("42"));
assert!(!db.is_empty());
}
#[test]
fn thin_provisioned_zero_is_false() {
let db = DiskDatabase::parse("ddb.thinProvisioned = \"0\"\n");
assert_eq!(db.thin_provisioned, Some(false));
}
#[test]
fn parses_bios_geometry() {
let db = DiskDatabase::parse(
"ddb.geometry.biosCylinders = \"100\"\nddb.geometry.biosHeads = \"8\"\nddb.geometry.biosSectors = \"32\"\n",
);
let g = db.bios_geometry.expect("bios geometry present");
assert_eq!(g.cylinders, 100);
assert_eq!(g.heads, 8);
assert_eq!(g.sectors, 32);
}
#[test]
fn ddb_line_without_equals_is_skipped() {
let db = DiskDatabase::parse("ddb.brokenline\nddb.adapterType = \"ide\"\n");
assert_eq!(db.adapter_type.as_deref(), Some("ide"));
}
#[test]
fn partial_geometry_is_ignored() {
let db =
DiskDatabase::parse("ddb.geometry.cylinders = \"100\"\nddb.geometry.heads = \"4\"\n");
assert_eq!(db.geometry, None);
}
}