1use std::{
4 fs::{File, OpenOptions},
5 path::{Path, PathBuf},
6 process::Command,
7};
8
9#[derive(Debug, Clone)]
11pub struct VirtualDrive {
12 filename: PathBuf,
13}
14
15impl VirtualDrive {
16 pub fn filename(&self) -> &Path {
18 self.filename.as_path()
19 }
20
21 pub fn extract_to(&self, dirname: &Path) -> Result<(), VirtualDriveError> {
24 if !dirname.exists() {
25 std::fs::create_dir(dirname)
26 .map_err(|e| VirtualDriveError::Extract(dirname.into(), e))?;
27 }
28 tar_extract(&self.filename, dirname)?;
29 Ok(())
30 }
31}
32
33#[derive(Debug, Default)]
35pub struct VirtualDriveBuilder {
36 filename: Option<PathBuf>,
37 root: Option<PathBuf>,
38 size: Option<u64>,
39}
40
41impl VirtualDriveBuilder {
42 pub fn filename(mut self, filename: &Path) -> Self {
44 self.filename = Some(filename.into());
45 self
46 }
47
48 pub fn root_directory(mut self, dirname: &Path) -> Self {
50 self.root = Some(dirname.into());
51 self
52 }
53
54 pub fn size(mut self, size: u64) -> Self {
57 self.size = Some(size);
58 self
59 }
60
61 pub fn create(self) -> Result<VirtualDrive, VirtualDriveError> {
63 let filename = self.filename.expect("filename has been set");
64
65 {
69 let archive = File::create(&filename)
70 .map_err(|e| VirtualDriveError::Create(filename.clone(), e))?;
71 if let Some(size) = self.size {
72 archive
73 .set_len(size)
74 .map_err(|e| VirtualDriveError::Create(filename.clone(), e))?;
75 }
76 }
77
78 if let Some(root) = self.root {
79 match tar_create(&filename, &root) {
80 Ok(_) => (),
81 Err(VirtualDriveError::TarFailed(cmd, filename, exit, stderr)) => {
82 Err(VirtualDriveError::TarFailed(cmd, filename, exit, stderr))?;
83 }
84 Err(err) => {
85 Err(err)?;
86 }
87 }
88 }
89
90 Ok(VirtualDrive { filename })
91 }
92
93 pub fn open(self) -> Result<VirtualDrive, VirtualDriveError> {
95 let filename = self.filename.expect("filename has been set");
96 Ok(VirtualDrive { filename })
97 }
98}
99
100pub fn create_tar(
102 tar_filename: PathBuf,
103 dirname: &Path,
104) -> Result<VirtualDrive, VirtualDriveError> {
105 assert!(!tar_filename.starts_with(dirname));
106 VirtualDriveBuilder::default()
107 .filename(&tar_filename)
108 .root_directory(dirname)
109 .create()
110}
111
112pub fn create_tar_with_size(
114 tar_filename: PathBuf,
115 dirname: &Path,
116 size: u64,
117) -> Result<VirtualDrive, VirtualDriveError> {
118 let tar = VirtualDriveBuilder::default()
119 .filename(&tar_filename)
120 .root_directory(dirname)
121 .size(size)
122 .create()?;
123
124 let metadata = std::fs::metadata(&tar_filename)
125 .map_err(|err| VirtualDriveError::Metadata(tar_filename.clone(), err))?;
126 if metadata.len() < size {
127 let file = OpenOptions::new()
128 .write(true)
129 .truncate(false)
130 .open(&tar_filename)
131 .map_err(|err| VirtualDriveError::CreateTar(tar_filename.clone(), err))?;
132 file.set_len(size)
133 .map_err(|err| VirtualDriveError::SetLen(size, tar_filename.clone(), err))?;
134 }
135
136 let metadata = std::fs::metadata(&tar_filename)
137 .map_err(|err| VirtualDriveError::Metadata(tar_filename.clone(), err))?;
138 if metadata.len() > size {
139 return Err(VirtualDriveError::DriveTooBig(metadata.len(), size));
140 }
141
142 Ok(tar)
143}
144
145fn tar_create(tar_filename: &Path, dirname: &Path) -> Result<(), VirtualDriveError> {
146 let output = Command::new("tar")
147 .arg("-cvf")
148 .arg(tar_filename)
149 .arg("-C")
150 .arg(dirname)
151 .arg(".")
152 .output()
153 .map_err(|err| VirtualDriveError::Tar("create", dirname.into(), err))?;
154
155 if let Some(exit) = output.status.code() {
156 if exit != 0 {
157 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
158 return Err(VirtualDriveError::TarFailed(
159 "create",
160 dirname.into(),
161 exit,
162 stderr,
163 ));
164 }
165 }
166
167 Ok(())
168}
169
170fn tar_extract(tar_filename: &Path, dirname: &Path) -> Result<(), VirtualDriveError> {
171 let output = Command::new("tar")
172 .arg("-xvvvf")
173 .arg(tar_filename)
174 .arg("-C")
175 .arg(dirname)
176 .arg("--no-same-owner")
177 .output()
178 .map_err(|err| VirtualDriveError::Tar("extract", dirname.into(), err))?;
179
180 if let Some(exit) = output.status.code() {
181 if exit != 0 {
182 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
183 return Err(VirtualDriveError::TarFailed(
184 "extract",
185 dirname.into(),
186 exit,
187 stderr,
188 ));
189 }
190 }
191
192 Ok(())
193}
194
195#[allow(missing_docs)]
197#[derive(Debug, thiserror::Error)]
198pub enum VirtualDriveError {
199 #[error("failed to create virtual drive {0}")]
200 Create(PathBuf, #[source] std::io::Error),
201
202 #[error("failed to create tar archive for virtual drive from {0}")]
203 CreateTar(PathBuf, #[source] std::io::Error),
204
205 #[error("failed to open virtual drive {0}")]
206 Open(PathBuf, #[source] std::io::Error),
207
208 #[error("failed to list files in virtual drive {0}")]
209 List(PathBuf, #[source] std::io::Error),
210
211 #[error("failed to create directory {0}")]
212 Extract(PathBuf, #[source] std::io::Error),
213
214 #[error("failed to extract {0} to {1}")]
215 ExtractEntry(PathBuf, PathBuf, #[source] std::io::Error),
216
217 #[error(transparent)]
218 Util(#[from] crate::util::UtilError),
219
220 #[error("failed to get length of file {0}")]
221 Metadata(PathBuf, #[source] std::io::Error),
222
223 #[error("failed to set length of file to {0}: {1}")]
224 SetLen(u64, PathBuf, #[source] std::io::Error),
225
226 #[error("virtual drive is too big: {0} > {1}")]
227 DriveTooBig(u64, u64),
228
229 #[error("failed to run system tar command: {0}: {1}")]
231 Tar(&'static str, PathBuf, #[source] std::io::Error),
232
233 #[error("failed to run system tar command: {0}: {1}")]
235 TarFailed(&'static str, PathBuf, i32, String),
236}