1#![expect(missing_docs)]
7#![forbid(unsafe_code)]
8
9use disk_backend::DiskError;
10use disk_backend::DiskIo;
11use disk_backend::resolve::ResolveDiskParameters;
12use disk_backend::resolve::ResolvedDisk;
13use disk_backend_resources::FixedVhd1DiskHandle;
14use disk_file::FileDisk;
15use guid::Guid;
16use inspect::Inspect;
17use scsi_buffers::RequestBuffers;
18use std::fs::File;
19use std::io;
20use std::io::Read;
21use std::io::Seek;
22use std::io::Write;
23use thiserror::Error;
24use vhd1_defs::VhdFooter;
25use vm_resource::ResolveResource;
26use vm_resource::declare_static_resolver;
27use vm_resource::kind::DiskHandleKind;
28use zerocopy::FromZeros;
29use zerocopy::IntoBytes;
30
31pub struct Vhd1Resolver;
32declare_static_resolver!(Vhd1Resolver, (DiskHandleKind, FixedVhd1DiskHandle));
33
34#[derive(Debug, Error)]
35pub enum ResolveVhd1DiskError {
36 #[error("failed to open VHD")]
37 Open(#[source] OpenError),
38 #[error("invalid disk")]
39 InvalidDisk(#[source] disk_backend::InvalidDisk),
40}
41
42impl ResolveResource<DiskHandleKind, FixedVhd1DiskHandle> for Vhd1Resolver {
43 type Output = ResolvedDisk;
44 type Error = ResolveVhd1DiskError;
45
46 fn resolve(
47 &self,
48 rsrc: FixedVhd1DiskHandle,
49 params: ResolveDiskParameters<'_>,
50 ) -> Result<Self::Output, Self::Error> {
51 let disk =
52 Vhd1Disk::open_fixed(rsrc.0, params.read_only).map_err(ResolveVhd1DiskError::Open)?;
53 ResolvedDisk::new(disk).map_err(ResolveVhd1DiskError::InvalidDisk)
54 }
55}
56
57#[derive(Debug, Inspect)]
59pub struct Vhd1Disk {
60 #[inspect(flatten)]
61 file: FileDisk,
62 unique_id: Guid,
63}
64
65const DEFAULT_SECTOR_SIZE: u32 = 512;
66const DEFAULT_PHYSICAL_SECTOR_SIZE: u32 = 512;
67
68#[derive(Debug)]
69struct Metadata {
70 disk_size: u64,
71 sector_size: u32,
72 unique_id: Guid,
73}
74
75impl Metadata {
76 fn from_footer(footer: VhdFooter, file_size: u64) -> Result<Metadata, OpenError> {
78 if footer.cookie != VhdFooter::COOKIE_MAGIC {
79 return Err(OpenError::InvalidFooterCookie);
80 }
81 if footer.checksum != footer.compute_checksum().to_be_bytes() {
82 return Err(OpenError::InvalidFooterChecksum);
83 }
84 if footer.file_format_version != VhdFooter::FILE_FORMAT_VERSION_MAGIC.to_be_bytes() {
85 return Err(OpenError::UnsupportedVersion(
86 footer.file_format_version.into(),
87 ));
88 }
89 if footer.disk_type != VhdFooter::DISK_TYPE_FIXED.to_be_bytes() {
91 return Err(OpenError::NotFixed);
92 }
93 let disk_size = footer.current_size.into();
94 let sector_size = DEFAULT_SECTOR_SIZE;
95 if disk_size > file_size - VhdFooter::LEN || disk_size % (sector_size as u64) != 0 {
96 return Err(OpenError::InvalidDiskSize(disk_size));
97 }
98
99 let unique_id = footer.unique_id;
100 Ok(Metadata {
101 disk_size,
102 sector_size,
103 unique_id,
104 })
105 }
106}
107
108#[derive(Debug, thiserror::Error)]
110#[non_exhaustive]
111pub enum OpenError {
112 #[error("invalid VHD file size: {0}")]
113 InvalidFileSize(u64),
114 #[error("invalid VHD disk size: {0}")]
115 InvalidDiskSize(u64),
116 #[error("io error")]
117 Io(#[from] io::Error),
118 #[error("VHD file footer is missing")]
119 InvalidFooterCookie,
120 #[error("invalid VHD footer checksum")]
121 InvalidFooterChecksum,
122 #[error("unsupported VHD version: {0:#x}")]
123 UnsupportedVersion(u32),
124 #[error("not a fixed VHD")]
125 NotFixed,
126}
127
128impl Vhd1Disk {
129 pub fn make_fixed(mut file: &File) -> Result<(), OpenError> {
131 let meta = file.metadata()?;
132 let len = meta.len();
133 if len % VhdFooter::ALIGNMENT != 0 {
134 return Err(OpenError::InvalidDiskSize(len));
135 }
136 file.seek(io::SeekFrom::End(0))?;
137 file.write_all(VhdFooter::new_fixed(len, Guid::new_random()).as_bytes())?;
138 Ok(())
139 }
140
141 pub fn open_fixed(mut file: File, read_only: bool) -> Result<Self, OpenError> {
143 let meta = file.metadata()?;
144 let len = meta.len();
145 if len < VhdFooter::LEN || len % VhdFooter::ALIGNMENT != 0 {
146 return Err(OpenError::InvalidFileSize(len));
147 }
148 file.seek(io::SeekFrom::End(-512))?;
149 let mut footer: VhdFooter = FromZeros::new_zeroed();
150 file.read_exact(footer.as_mut_bytes())?;
151 let metadata = Metadata::from_footer(footer, len)?;
152
153 let file = FileDisk::with_metadata(
155 file,
156 disk_file::Metadata {
157 disk_size: metadata.disk_size,
158 sector_size: metadata.sector_size,
159 physical_sector_size: DEFAULT_PHYSICAL_SECTOR_SIZE,
160 read_only,
161 },
162 );
163
164 Ok(Self {
165 file,
166 unique_id: metadata.unique_id,
167 })
168 }
169
170 pub fn into_inner(self) -> File {
172 self.file.into_inner()
173 }
174}
175
176impl DiskIo for Vhd1Disk {
177 fn disk_type(&self) -> &str {
178 "vhd1"
179 }
180
181 fn sector_count(&self) -> u64 {
182 self.file.sector_count()
183 }
184
185 fn sector_size(&self) -> u32 {
186 self.file.sector_size()
187 }
188
189 fn is_read_only(&self) -> bool {
190 self.file.is_read_only()
191 }
192
193 fn disk_id(&self) -> Option<[u8; 16]> {
194 Some(self.unique_id.into())
195 }
196
197 fn physical_sector_size(&self) -> u32 {
198 self.file.physical_sector_size()
199 }
200
201 fn is_fua_respected(&self) -> bool {
202 self.file.is_fua_respected()
203 }
204
205 async fn read_vectored(
206 &self,
207 buffers: &RequestBuffers<'_>,
208 sector: u64,
209 ) -> Result<(), DiskError> {
210 self.file.read_vectored(buffers, sector).await
211 }
212
213 async fn write_vectored(
214 &self,
215 buffers: &RequestBuffers<'_>,
216 sector: u64,
217 fua: bool,
218 ) -> Result<(), DiskError> {
219 self.file.write_vectored(buffers, sector, fua).await
220 }
221
222 async fn sync_cache(&self) -> Result<(), DiskError> {
223 self.file.sync_cache().await
224 }
225
226 async fn unmap(
227 &self,
228 _sector: u64,
229 _count: u64,
230 _block_level_only: bool,
231 ) -> Result<(), DiskError> {
232 Ok(())
233 }
234
235 fn unmap_behavior(&self) -> disk_backend::UnmapBehavior {
236 disk_backend::UnmapBehavior::Ignored
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::Vhd1Disk;
243 use disk_backend::Disk;
244 use guestmem::GuestMemory;
245 use pal_async::async_test;
246 use scsi_buffers::OwnedRequestBuffers;
247 use std::io::Write;
248 use zerocopy::IntoBytes;
249
250 #[async_test]
251 async fn open_fixed() {
252 let mut file = tempfile::tempfile().unwrap();
253 let data = (0..0x100000_u32).collect::<Vec<_>>();
254 file.write_all(data.as_bytes()).unwrap();
255 Vhd1Disk::make_fixed(&file).unwrap();
256 let vhd = Disk::new(Vhd1Disk::open_fixed(file, false).unwrap()).unwrap();
257
258 let mem = GuestMemory::allocate(0x1000);
259
260 let mut buf = [0_u32; 128];
261 vhd.read_vectored(
262 &OwnedRequestBuffers::linear(0, 512, true).buffer(&mem),
263 1000,
264 )
265 .await
266 .unwrap();
267 mem.read_at(0, buf.as_mut_bytes()).unwrap();
268 assert!(buf.iter().copied().eq(1000_u32 * 128..1001 * 128));
269 }
270}