1use anyhow::Context;
7use std::path::Path;
8use vm_resource::Resource;
9use vm_resource::kind::DiskHandleKind;
10
11fn disk_open_error(path: &Path, verb: &str) -> String {
12 let mut msg = format!("{verb} '{}'", path.display());
13
14 if cfg!(windows) && std::env::var_os("WSLENV").is_some() {
17 msg += ". Linux paths are not supported when running Windows executables \
18 under WSL, make sure the path is a valid Windows path \
19 (use `wslpath -w` to convert)";
20 }
21
22 msg
23}
24
25#[derive(Clone, Copy)]
27pub struct OpenDiskOptions {
28 pub read_only: bool,
30 pub direct: bool,
32}
33
34pub async fn open_disk_type(
46 path: &Path,
47 options: OpenDiskOptions,
48) -> anyhow::Result<Resource<DiskHandleKind>> {
49 let read_only = options.read_only;
50 let ensure_no_direct = |ext| {
51 if options.direct {
52 anyhow::bail!("direct I/O is not supported for {ext} files");
53 };
54 Ok(())
55 };
56 Ok(match path.extension().and_then(|s| s.to_str()) {
57 Some("vmdk") => {
58 ensure_no_direct(".vmdk")?;
59 disk_vmdk::open_vmdk(path, read_only)
60 .with_context(|| disk_open_error(path, "failed to open"))?
61 }
62 Some("vhd") => {
63 let file = std::fs::OpenOptions::new()
64 .read(true)
65 .write(!read_only)
66 .open(path)
67 .with_context(|| disk_open_error(path, "failed to open"))?;
68
69 match disk_vhd1::Vhd1Disk::open_fixed(file, read_only) {
70 Ok(vhd) => {
71 ensure_no_direct("fixed .vhd")?;
72 Resource::new(disk_backend_resources::FixedVhd1DiskHandle(
73 vhd.into_inner(),
74 ))
75 }
76 Err(disk_vhd1::OpenError::NotFixed) => {
77 #[cfg(windows)]
78 {
79 Resource::new(disk_vhdmp::OpenVhdmpDiskConfig(
80 disk_vhdmp::VhdmpDisk::options()
81 .read_only(read_only)
82 .cached_io(!options.direct)
83 .open(path)
84 .with_context(|| disk_open_error(path, "failed to open"))?,
85 ))
86 }
87 #[cfg(not(windows))]
88 anyhow::bail!("non-fixed VHD not supported on Linux");
89 }
90 Err(err) => return Err(err.into()),
91 }
92 }
93 Some("vhdx") => {
94 #[cfg(windows)]
95 {
96 Resource::new(disk_vhdmp::OpenVhdmpDiskConfig(
97 disk_vhdmp::VhdmpDisk::options()
98 .read_only(read_only)
99 .cached_io(!options.direct)
100 .open(path)
101 .with_context(|| disk_open_error(path, "failed to open"))?,
102 ))
103 }
104 #[cfg(not(windows))]
105 {
106 ensure_no_direct(".vhdx")?;
107 disklayer_vhdx::chain::open_vhdx_chain(path, read_only).await?
108 }
109 }
110 Some("iso") if !read_only => {
111 anyhow::bail!("iso file cannot be opened as read/write")
112 }
113 Some("vmgs") => {
114 ensure_no_direct(".vmgs")?;
115 let file = std::fs::OpenOptions::new()
118 .read(true)
119 .write(!read_only)
120 .open(path)
121 .with_context(|| disk_open_error(path, "failed to open"))?;
122
123 Resource::new(disk_backend_resources::FixedVhd1DiskHandle(file))
124 }
125 _ => open_raw_disk(path, options, None)?,
126 })
127}
128
129pub fn create_disk_type(
131 path: &Path,
132 size: u64,
133 options: OpenDiskOptions,
134) -> anyhow::Result<Resource<DiskHandleKind>> {
135 Ok(match path.extension().and_then(|s| s.to_str()) {
136 Some("vhd") | Some("vmgs") => {
137 if options.direct {
138 anyhow::bail!("direct I/O is not supported for VHD files");
139 }
140 let file = std::fs::OpenOptions::new()
141 .create(true)
142 .truncate(true)
143 .read(true)
144 .write(true)
145 .open(path)
146 .with_context(|| disk_open_error(path, "failed to create"))?;
147
148 file.set_len(size)?;
149 disk_vhd1::Vhd1Disk::make_fixed(&file)?;
150 Resource::new(disk_backend_resources::FixedVhd1DiskHandle(file))
151 }
152 Some("vhdx") => {
153 anyhow::bail!("creating vhdx not supported")
154 }
155 Some("vmdk") => {
156 anyhow::bail!("creating vmdk not supported")
157 }
158 Some("iso") => {
159 anyhow::bail!("creating iso not supported")
160 }
161 _ => open_raw_disk(path, options, Some(size))?,
162 })
163}
164
165fn open_raw_disk(
168 path: &Path,
169 options: OpenDiskOptions,
170 size: Option<u64>,
171) -> anyhow::Result<Resource<DiskHandleKind>> {
172 if options.direct && !cfg!(target_os = "linux") {
173 anyhow::bail!("direct I/O is only supported on Linux");
174 }
175
176 let create = size.is_some();
177 let mut opts = std::fs::OpenOptions::new();
178 opts.read(true).write(!options.read_only);
179 if create {
180 opts.create(true).truncate(true);
181 }
182
183 #[cfg(target_os = "linux")]
184 if options.direct {
185 use std::os::unix::fs::OpenOptionsExt;
186 opts.custom_flags(libc::O_DIRECT);
187 }
188
189 let verb = if create {
190 "failed to create"
191 } else {
192 "failed to open"
193 };
194 let file = opts
195 .open(path)
196 .with_context(|| disk_open_error(path, verb))?;
197
198 if let Some(size) = size {
199 file.set_len(size)?;
200 }
201
202 #[cfg(target_os = "linux")]
203 {
204 Ok(Resource::new(
205 disk_backend_resources::BlockDeviceDiskHandle { file },
206 ))
207 }
208 #[cfg(not(target_os = "linux"))]
209 {
210 Ok(Resource::new(disk_backend_resources::FileDiskHandle(file)))
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use pal_async::async_test;
218
219 fn create_test_vmdk() -> (tempfile::TempDir, std::path::PathBuf) {
221 let directory = tempfile::tempdir().unwrap();
222 std::fs::write(directory.path().join("source.bin"), [0; 512]).unwrap();
223 let descriptor = directory.path().join("disk.vmdk");
224 std::fs::write(
225 &descriptor,
226 "# Disk DescriptorFile\n\
227 version=1\n\
228 parentCID=ffffffff\n\
229 createType=\"monolithicFlat\"\n\
230 # Extent description\n\
231 RW 1 FLAT \"source.bin\" 0\n",
232 )
233 .unwrap();
234 (directory, descriptor)
235 }
236
237 #[async_test]
238 async fn opens_flat_vmdk_read_only() {
239 let (_directory, descriptor) = create_test_vmdk();
240 open_disk_type(
241 &descriptor,
242 OpenDiskOptions {
243 read_only: true,
244 direct: false,
245 },
246 )
247 .await
248 .unwrap();
249 }
250
251 #[async_test]
252 async fn rejects_vmdk_write_direct_io_and_creation() {
253 let (_directory, descriptor) = create_test_vmdk();
254 assert!(
256 open_disk_type(
257 &descriptor,
258 OpenDiskOptions {
259 read_only: false,
260 direct: false,
261 },
262 )
263 .await
264 .is_err()
265 );
266 assert!(
268 open_disk_type(
269 &descriptor,
270 OpenDiskOptions {
271 read_only: true,
272 direct: true,
273 },
274 )
275 .await
276 .is_err()
277 );
278 assert!(
280 create_disk_type(
281 &descriptor,
282 512,
283 OpenDiskOptions {
284 read_only: false,
285 direct: false,
286 },
287 )
288 .is_err()
289 );
290 }
291}