/ src / vdrive.rs
vdrive.rs
  1  //! Virtual drive handling for ambient-run.
  2  
  3  use std::{
  4      fs::{File, OpenOptions},
  5      path::{Path, PathBuf},
  6      process::Command,
  7  };
  8  
  9  /// A virtual drive.
 10  #[derive(Debug, Clone)]
 11  pub struct VirtualDrive {
 12      filename: PathBuf,
 13  }
 14  
 15  impl VirtualDrive {
 16      /// Path to file containing virtual drive.
 17      pub fn filename(&self) -> &Path {
 18          self.filename.as_path()
 19      }
 20  
 21      /// Extract files in the virtual drive into a directory. Create
 22      /// the directory if it doesn't exist.
 23      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  /// Builder for [`VirtualDrive`].
 34  #[derive(Debug, Default)]
 35  pub struct VirtualDriveBuilder {
 36      filename: Option<PathBuf>,
 37      root: Option<PathBuf>,
 38      size: Option<u64>,
 39  }
 40  
 41  impl VirtualDriveBuilder {
 42      /// Set filename for virtual drive.
 43      pub fn filename(mut self, filename: &Path) -> Self {
 44          self.filename = Some(filename.into());
 45          self
 46      }
 47  
 48      /// Set directory of tree to copy into virtual drive.
 49      pub fn root_directory(mut self, dirname: &Path) -> Self {
 50          self.root = Some(dirname.into());
 51          self
 52      }
 53  
 54      /// Set size of new drive. This is important when the build VM
 55      /// writes to the drive.
 56      pub fn size(mut self, size: u64) -> Self {
 57          self.size = Some(size);
 58          self
 59      }
 60  
 61      /// Create a virtual drive.
 62      pub fn create(self) -> Result<VirtualDrive, VirtualDriveError> {
 63          let filename = self.filename.expect("filename has been set");
 64  
 65          // Create the file, either empty or to the desired size. If we
 66          // don't have self.root set, the file created (and maybe
 67          // truncated) will be.
 68          {
 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      /// Open an existing virtual drive.
 94      pub fn open(self) -> Result<VirtualDrive, VirtualDriveError> {
 95          let filename = self.filename.expect("filename has been set");
 96          Ok(VirtualDrive { filename })
 97      }
 98  }
 99  
100  /// Create a tar archive out of a directory.
101  pub 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  
112  /// Create a tar archive with a fixed length, out of a directory.
113  pub 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  
145  fn 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  
170  fn 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  /// Errors that may be returned from [`VirtualDrive`] use.
196  #[allow(missing_docs)]
197  #[derive(Debug, thiserror::Error)]
198  pub 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      /// Can't run tar.
230      #[error("failed to run system tar command: {0}: {1}")]
231      Tar(&'static str, PathBuf, #[source] std::io::Error),
232  
233      /// Tar failed.
234      #[error("failed to run system tar command: {0}: {1}")]
235      TarFailed(&'static str, PathBuf, i32, String),
236  }