1#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct DiskGeometry {
17 pub cylinders: u32,
18 pub heads: u32,
19 pub sectors: u32,
20}
21
22impl DiskGeometry {
23 #[must_use]
25 pub fn chs_sectors(&self) -> u64 {
26 u64::from(self.cylinders) * u64::from(self.heads) * u64::from(self.sectors)
27 }
28}
29
30#[derive(Debug, Clone, Default, PartialEq, Eq)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37pub struct DiskDatabase {
38 pub adapter_type: Option<String>,
40 pub geometry: Option<DiskGeometry>,
42 pub bios_geometry: Option<DiskGeometry>,
44 pub virtual_hw_version: Option<String>,
46 pub tools_version: Option<String>,
48 pub uuid: Option<String>,
50 pub long_content_id: Option<String>,
52 pub thin_provisioned: Option<bool>,
54 pub encoding: Option<String>,
56 pub entries: Vec<(String, String)>,
58}
59
60impl DiskDatabase {
61 #[must_use]
63 pub fn parse(descriptor_text: &str) -> Self {
64 let mut db = DiskDatabase::default();
65 let (mut cyl, mut head, mut sect) = (None, None, None);
66 let (mut bcyl, mut bhead, mut bsect) = (None, None, None);
67
68 for line in descriptor_text.lines() {
69 let line = line.trim();
70 let Some(rest) = line.strip_prefix("ddb.") else {
71 continue;
72 };
73 let Some((key, value)) = rest.split_once('=') else {
75 continue;
76 };
77 let key = key.trim();
78 let value = value.trim().trim_matches('"').to_owned();
79 let full_key = format!("ddb.{key}");
80 db.entries.push((full_key, value.clone()));
81
82 match key {
83 "adapterType" => db.adapter_type = Some(value),
84 "virtualHWVersion" => db.virtual_hw_version = Some(value),
85 "toolsVersion" => db.tools_version = Some(value),
86 "uuid" => db.uuid = Some(value),
87 "longContentID" => db.long_content_id = Some(value),
88 "encoding" => db.encoding = Some(value),
89 "thinProvisioned" => db.thin_provisioned = Some(value.trim() == "1"),
90 "geometry.cylinders" => cyl = value.parse().ok(),
91 "geometry.heads" => head = value.parse().ok(),
92 "geometry.sectors" => sect = value.parse().ok(),
93 "geometry.biosCylinders" => bcyl = value.parse().ok(),
94 "geometry.biosHeads" => bhead = value.parse().ok(),
95 "geometry.biosSectors" => bsect = value.parse().ok(),
96 _ => {}
97 }
98 }
99
100 if let (Some(cylinders), Some(heads), Some(sectors)) = (cyl, head, sect) {
101 db.geometry = Some(DiskGeometry {
102 cylinders,
103 heads,
104 sectors,
105 });
106 }
107 if let (Some(cylinders), Some(heads), Some(sectors)) = (bcyl, bhead, bsect) {
108 db.bios_geometry = Some(DiskGeometry {
109 cylinders,
110 heads,
111 sectors,
112 });
113 }
114 db
115 }
116
117 #[must_use]
119 pub fn is_empty(&self) -> bool {
120 self.entries.is_empty()
121 }
122
123 #[must_use]
125 pub fn get(&self, key: &str) -> Option<&str> {
126 self.entries
127 .iter()
128 .find(|(k, _)| k == key)
129 .map(|(_, v)| v.as_str())
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 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";
138
139 #[test]
140 fn parses_adapter_type_and_versions() {
141 let db = DiskDatabase::parse(FULL);
142 assert_eq!(db.adapter_type.as_deref(), Some("lsilogic"));
143 assert_eq!(db.virtual_hw_version.as_deref(), Some("7"));
144 assert_eq!(db.tools_version.as_deref(), Some("10338"));
145 assert_eq!(db.encoding.as_deref(), Some("UTF-8"));
146 }
147
148 #[test]
149 fn parses_geometry() {
150 let db = DiskDatabase::parse(FULL);
151 let g = db.geometry.expect("geometry present");
152 assert_eq!(g.cylinders, 16383);
153 assert_eq!(g.heads, 16);
154 assert_eq!(g.sectors, 63);
155 assert_eq!(g.chs_sectors(), 16383 * 16 * 63);
157 }
158
159 #[test]
160 fn parses_uuid_thin_and_long_content_id() {
161 let db = DiskDatabase::parse(FULL);
162 assert_eq!(
163 db.uuid.as_deref(),
164 Some("60 00 C2 97 1a 2b 3c 4d-5e 6f 70 81 92 a3 b4 c5")
165 );
166 assert_eq!(
167 db.long_content_id.as_deref(),
168 Some("deadbeefcafef00d1122334455667788")
169 );
170 assert_eq!(db.thin_provisioned, Some(true));
171 }
172
173 #[test]
174 fn empty_when_no_ddb_section() {
175 let db = DiskDatabase::parse(
176 "# Disk DescriptorFile\nversion=1\ncreateType=\"monolithicFlat\"\n",
177 );
178 assert!(db.is_empty());
179 assert_eq!(db.adapter_type, None);
180 assert_eq!(db.geometry, None);
181 assert_eq!(db.thin_provisioned, None);
182 }
183
184 #[test]
185 fn unknown_ddb_keys_are_retained() {
186 let db = DiskDatabase::parse("ddb.somethingNew = \"42\"\nddb.adapterType = \"ide\"\n");
187 assert_eq!(db.adapter_type.as_deref(), Some("ide"));
188 assert_eq!(db.get("ddb.somethingNew"), Some("42"));
189 assert!(!db.is_empty());
190 }
191
192 #[test]
193 fn thin_provisioned_zero_is_false() {
194 let db = DiskDatabase::parse("ddb.thinProvisioned = \"0\"\n");
195 assert_eq!(db.thin_provisioned, Some(false));
196 }
197
198 #[test]
199 fn parses_bios_geometry() {
200 let db = DiskDatabase::parse(
201 "ddb.geometry.biosCylinders = \"100\"\nddb.geometry.biosHeads = \"8\"\nddb.geometry.biosSectors = \"32\"\n",
202 );
203 let g = db.bios_geometry.expect("bios geometry present");
204 assert_eq!(g.cylinders, 100);
205 assert_eq!(g.heads, 8);
206 assert_eq!(g.sectors, 32);
207 }
208
209 #[test]
210 fn ddb_line_without_equals_is_skipped() {
211 let db = DiskDatabase::parse("ddb.brokenline\nddb.adapterType = \"ide\"\n");
213 assert_eq!(db.adapter_type.as_deref(), Some("ide"));
214 }
215
216 #[test]
217 fn partial_geometry_is_ignored() {
218 let db =
220 DiskDatabase::parse("ddb.geometry.cylinders = \"100\"\nddb.geometry.heads = \"4\"\n");
221 assert_eq!(db.geometry, None);
222 }
223}