1use cfg_if::cfg_if;
22use std::error::Error as StdError;
23use std::ffi::{OsStr, OsString};
24use std::fmt::{self, Display, Formatter};
25use std::path::{Component, Path, PathBuf};
26use std::sync::{Mutex, MutexGuard};
27use std::time::Instant;
28
29static ENV_LOCK: Mutex<()> = Mutex::new(());
33
34#[derive(Debug)]
39pub enum Error {
40 Inactive,
42 Io {
45 action: String,
47 path: Option<PathBuf>,
49 source: std::io::Error,
51 },
52 NotRelative {
54 path: PathBuf,
56 },
57 Escapes {
59 path: PathBuf,
61 },
62 Other(Box<dyn StdError + Send + Sync>),
64}
65
66impl Display for Error {
67 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
68 match self {
69 Error::Inactive => write!(
70 f,
71 "the sandbox environment is not active; call this inside `Environment::run`"
72 )?,
73 Error::Io {
74 action,
75 path,
76 source: _,
77 } => match path {
78 Some(path) => write!(f, "could not {action} `{}`", path.display())?,
79 None => write!(f, "could not {action}")?,
80 },
81 Error::NotRelative { path } => write!(
82 f,
83 "path `{}` must be relative to the sandbox",
84 path.display()
85 )?,
86 Error::Escapes { path } => {
87 write!(f, "path `{}` escapes the sandbox directory", path.display())?
88 }
89 Error::Other(source) => write!(f, "{source}")?,
90 }
91 if f.alternate() {
92 let mut cause = StdError::source(self);
93 while let Some(error) = cause {
94 write!(f, ": {error}")?;
95 cause = error.source();
96 }
97 }
98 Ok(())
99 }
100}
101
102impl StdError for Error {
103 fn source(&self) -> Option<&(dyn StdError + 'static)> {
104 match self {
105 Error::Io { source, .. } => Some(source),
106 Error::Other(source) => source.source(),
107 Error::Inactive | Error::NotRelative { .. } | Error::Escapes { .. } => None,
108 }
109 }
110}
111
112impl From<Box<dyn StdError + Send + Sync>> for Error {
113 fn from(source: Box<dyn StdError + Send + Sync>) -> Self {
114 Error::Other(source)
115 }
116}
117
118impl From<std::io::Error> for Error {
119 fn from(source: std::io::Error) -> Self {
120 Error::Other(Box::new(source))
121 }
122}
123
124pub struct Environment {
127 entered: Option<Entered>,
128}
129
130struct Entered {
132 directory: PathBuf,
133 saved_cwd: PathBuf,
134 saved_env: Vec<(OsString, OsString)>,
135 started: Instant,
136 _guard: MutexGuard<'static, ()>,
139}
140
141pub fn run<T>(f: impl FnOnce(&mut Environment) -> Result<T, Error>) -> Result<T, Error> {
144 Environment::temporary().run(f)
145}
146
147impl Environment {
148 pub fn temporary() -> Self {
151 Self { entered: None }
152 }
153
154 pub fn directory(&self) -> Option<&Path> {
157 match &self.entered {
158 Some(entered) => Some(&entered.directory),
159 None => None,
160 }
161 }
162
163 pub fn run<T>(
173 mut self,
174 f: impl FnOnce(&mut Environment) -> Result<T, Error>,
175 ) -> Result<T, Error> {
176 let guard = match ENV_LOCK.lock() {
177 Ok(guard) => guard,
178 Err(poisoned) => poisoned.into_inner(),
179 };
180 let started = Instant::now();
181
182 cfg_if! {
183 if #[cfg(feature = "tracing")] {
184 tracing::debug!(msg = "Entering sandbox environment");
185 } else if #[cfg(feature = "logging")] {
186 log::debug!("msg=\"Entering sandbox environment\"");
187 }
188 }
189
190 let saved_cwd = match std::env::current_dir() {
191 Ok(cwd) => cwd,
192 Err(source) => {
193 return Err(Error::Io {
194 action: String::from("read the current directory"),
195 path: None,
196 source,
197 });
198 }
199 };
200 let saved_env = std::env::vars_os().collect();
201
202 let nanos = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
203 Ok(elapsed) => elapsed.as_nanos(),
204 Err(_) => 0,
205 };
206 let name = format!("tanzim-testing-{}-{}", std::process::id(), nanos);
207
208 let target = std::env::temp_dir().join(&name);
209 let created = match std::fs::create_dir(&target) {
210 Ok(()) => target,
211 Err(source) => {
212 if source.kind() != std::io::ErrorKind::PermissionDenied {
213 return Err(Error::Io {
214 action: String::from("create the sandbox directory"),
215 path: Some(target),
216 source,
217 });
218 }
219 let fallback = saved_cwd.join(&name);
220 match std::fs::create_dir(&fallback) {
221 Ok(()) => fallback,
222 Err(source) => {
223 return Err(Error::Io {
224 action: String::from("create the sandbox directory"),
225 path: Some(fallback),
226 source,
227 });
228 }
229 }
230 }
231 };
232 let directory = match std::fs::canonicalize(&created) {
233 Ok(directory) => directory,
234 Err(source) => {
235 let _ = std::fs::remove_dir_all(&created);
236 return Err(Error::Io {
237 action: String::from("resolve the sandbox directory"),
238 path: Some(created),
239 source,
240 });
241 }
242 };
243
244 cfg_if! {
245 if #[cfg(feature = "tracing")] {
246 tracing::info!(msg = "Created sandbox directory", path = ?directory);
247 } else if #[cfg(feature = "logging")] {
248 log::info!("msg=\"Created sandbox directory\" path={directory:?}");
249 }
250 }
251
252 let cwd_target = directory.clone();
253 self.entered = Some(Entered {
254 directory,
255 saved_cwd,
256 saved_env,
257 started,
258 _guard: guard,
259 });
260
261 match std::env::set_current_dir(&cwd_target) {
262 Ok(()) => {}
263 Err(source) => {
264 return Err(Error::Io {
265 action: String::from("enter the sandbox directory"),
266 path: Some(cwd_target),
267 source,
268 });
269 }
270 }
271
272 cfg_if! {
273 if #[cfg(feature = "tracing")] {
274 tracing::trace!(msg = "Changed working directory into sandbox", path = ?cwd_target);
275 } else if #[cfg(feature = "logging")] {
276 log::trace!("msg=\"Changed working directory into sandbox\" path={cwd_target:?}");
277 }
278 }
279
280 f(&mut self)
281 }
282
283 pub fn clear_env(&mut self) {
287 if self.entered.is_none() {
288 return;
289 }
290
291 cfg_if! {
292 if #[cfg(feature = "tracing")] {
293 tracing::debug!(msg = "Clearing environment variables");
294 } else if #[cfg(feature = "logging")] {
295 log::debug!("msg=\"Clearing environment variables\"");
296 }
297 }
298
299 for (key, _) in std::env::vars_os() {
300 unsafe { std::env::remove_var(&key) };
302 }
303 }
304
305 pub fn set_env(
310 &mut self,
311 key: impl AsRef<OsStr>,
312 value: impl AsRef<OsStr>,
313 ) -> Result<(), Error> {
314 if self.entered.is_none() {
315 return Err(Error::Inactive);
316 }
317 let key = key.as_ref();
318 let value = value.as_ref();
319
320 cfg_if! {
321 if #[cfg(feature = "tracing")] {
322 tracing::debug!(msg = "Setting environment variable", key = ?key);
323 } else if #[cfg(feature = "logging")] {
324 log::debug!("msg=\"Setting environment variable\" key={key:?}");
325 }
326 }
327
328 unsafe { std::env::set_var(key, value) };
330
331 cfg_if! {
332 if #[cfg(feature = "tracing")] {
333 tracing::trace!(msg = "Set environment variable value", key = ?key, value = ?value);
334 } else if #[cfg(feature = "logging")] {
335 log::trace!("msg=\"Set environment variable value\" key={key:?} value={value:?}");
336 }
337 }
338 cfg_if! {
339 if #[cfg(feature = "tracing")] {
340 tracing::info!(msg = "Set environment variable", key = ?key);
341 } else if #[cfg(feature = "logging")] {
342 log::info!("msg=\"Set environment variable\" key={key:?}");
343 }
344 }
345 Ok(())
346 }
347
348 pub fn create_file(&mut self, path: impl AsRef<Path>) -> Result<(), Error> {
352 let directory = match &self.entered {
353 Some(entered) => entered.directory.clone(),
354 None => return Err(Error::Inactive),
355 };
356 let full = resolve(&directory, path.as_ref())?;
357 let _existed = full.exists();
358
359 cfg_if! {
360 if #[cfg(feature = "tracing")] {
361 tracing::debug!(msg = "Creating file", path = ?full);
362 } else if #[cfg(feature = "logging")] {
363 log::debug!("msg=\"Creating file\" path={full:?}");
364 }
365 }
366
367 create_parents(&full)?;
368 match std::fs::File::create(&full) {
369 Ok(_) => {}
370 Err(source) => {
371 return Err(Error::Io {
372 action: String::from("create the file"),
373 path: Some(full),
374 source,
375 });
376 }
377 }
378 confirm_within(&directory, &full)?;
379
380 cfg_if! {
381 if #[cfg(feature = "tracing")] {
382 tracing::info!(msg = "Created file", path = ?full, recreated = _existed);
383 } else if #[cfg(feature = "logging")] {
384 log::info!("msg=\"Created file\" path={full:?} recreated={_existed}");
385 }
386 }
387 Ok(())
388 }
389
390 pub fn write_file(
393 &mut self,
394 path: impl AsRef<Path>,
395 contents: impl AsRef<[u8]>,
396 ) -> Result<(), Error> {
397 let directory = match &self.entered {
398 Some(entered) => entered.directory.clone(),
399 None => return Err(Error::Inactive),
400 };
401 let full = resolve(&directory, path.as_ref())?;
402 let bytes = contents.as_ref();
403 let _existed = full.exists();
404
405 cfg_if! {
406 if #[cfg(feature = "tracing")] {
407 tracing::debug!(msg = "Writing file", path = ?full, bytes = bytes.len());
408 } else if #[cfg(feature = "logging")] {
409 log::debug!("msg=\"Writing file\" path={full:?} bytes={}", bytes.len());
410 }
411 }
412
413 create_parents(&full)?;
414 match std::fs::write(&full, bytes) {
415 Ok(()) => {}
416 Err(source) => {
417 return Err(Error::Io {
418 action: String::from("write the file"),
419 path: Some(full),
420 source,
421 });
422 }
423 }
424 confirm_within(&directory, &full)?;
425
426 cfg_if! {
427 if #[cfg(feature = "tracing")] {
428 tracing::trace!(
429 msg = "Wrote file contents",
430 path = ?full,
431 contents = %String::from_utf8_lossy(bytes),
432 );
433 } else if #[cfg(feature = "logging")] {
434 log::trace!(
435 "msg=\"Wrote file contents\" path={full:?} contents={}",
436 String::from_utf8_lossy(bytes),
437 );
438 }
439 }
440 cfg_if! {
441 if #[cfg(feature = "tracing")] {
442 tracing::info!(
443 msg = "Wrote file",
444 path = ?full,
445 bytes = bytes.len(),
446 recreated = _existed,
447 );
448 } else if #[cfg(feature = "logging")] {
449 log::info!(
450 "msg=\"Wrote file\" path={full:?} bytes={} recreated={_existed}",
451 bytes.len(),
452 );
453 }
454 }
455 Ok(())
456 }
457
458 pub fn create_directory(&mut self, path: impl AsRef<Path>) -> Result<(), Error> {
460 let directory = match &self.entered {
461 Some(entered) => entered.directory.clone(),
462 None => return Err(Error::Inactive),
463 };
464 let full = resolve(&directory, path.as_ref())?;
465 let _existed = full.exists();
466
467 cfg_if! {
468 if #[cfg(feature = "tracing")] {
469 tracing::debug!(msg = "Creating directory", path = ?full);
470 } else if #[cfg(feature = "logging")] {
471 log::debug!("msg=\"Creating directory\" path={full:?}");
472 }
473 }
474
475 match std::fs::create_dir_all(&full) {
476 Ok(()) => {}
477 Err(source) => {
478 return Err(Error::Io {
479 action: String::from("create the directory"),
480 path: Some(full),
481 source,
482 });
483 }
484 }
485 confirm_within(&directory, &full)?;
486
487 cfg_if! {
488 if #[cfg(feature = "tracing")] {
489 tracing::info!(msg = "Created directory", path = ?full, recreated = _existed);
490 } else if #[cfg(feature = "logging")] {
491 log::info!("msg=\"Created directory\" path={full:?} recreated={_existed}");
492 }
493 }
494 Ok(())
495 }
496}
497
498impl Drop for Environment {
499 fn drop(&mut self) {
500 let entered = match self.entered.take() {
501 Some(entered) => entered,
502 None => return,
503 };
504
505 cfg_if! {
506 if #[cfg(feature = "tracing")] {
507 tracing::trace!(msg = "Restoring environment and removing sandbox");
508 } else if #[cfg(feature = "logging")] {
509 log::trace!("msg=\"Restoring environment and removing sandbox\"");
510 }
511 }
512
513 for (key, _) in std::env::vars_os() {
514 unsafe { std::env::remove_var(&key) };
516 }
517 for (key, value) in &entered.saved_env {
518 unsafe { std::env::set_var(key, value) };
520 }
521
522 match std::env::set_current_dir(&entered.saved_cwd) {
523 Ok(()) => {}
524 Err(_source) => {
525 cfg_if! {
526 if #[cfg(feature = "tracing")] {
527 tracing::warn!(
528 msg = "Could not restore working directory",
529 path = ?entered.saved_cwd,
530 error = ?_source,
531 );
532 } else if #[cfg(feature = "logging")] {
533 log::warn!(
534 "msg=\"Could not restore working directory\" path={:?} error={_source:?}",
535 entered.saved_cwd,
536 );
537 }
538 }
539 }
540 }
541
542 match std::fs::remove_dir_all(&entered.directory) {
543 Ok(()) => {
544 cfg_if! {
545 if #[cfg(feature = "tracing")] {
546 tracing::info!(msg = "Removed sandbox directory", path = ?entered.directory);
547 } else if #[cfg(feature = "logging")] {
548 log::info!(
549 "msg=\"Removed sandbox directory\" path={:?}",
550 entered.directory,
551 );
552 }
553 }
554 }
555 Err(_source) => {
556 cfg_if! {
557 if #[cfg(feature = "tracing")] {
558 tracing::warn!(
559 msg = "Could not remove sandbox directory",
560 path = ?entered.directory,
561 error = ?_source,
562 );
563 } else if #[cfg(feature = "logging")] {
564 log::warn!(
565 "msg=\"Could not remove sandbox directory\" path={:?} error={_source:?}",
566 entered.directory,
567 );
568 }
569 }
570 }
571 }
572
573 let _held = entered.started.elapsed();
574 cfg_if! {
575 if #[cfg(feature = "tracing")] {
576 tracing::info!(msg = "Released sandbox lock", held_seconds = _held.as_secs_f64());
577 } else if #[cfg(feature = "logging")] {
578 log::info!("msg=\"Released sandbox lock\" held_seconds={}", _held.as_secs_f64());
579 }
580 }
581 }
582}
583
584fn resolve(directory: &Path, relative: &Path) -> Result<PathBuf, Error> {
587 if relative.is_absolute() {
588 return Err(Error::NotRelative {
589 path: relative.to_path_buf(),
590 });
591 }
592 for component in relative.components() {
593 if matches!(component, Component::ParentDir) {
594 return Err(Error::Escapes {
595 path: relative.to_path_buf(),
596 });
597 }
598 }
599 Ok(directory.join(relative))
600}
601
602fn create_parents(full: &Path) -> Result<(), Error> {
604 match full.parent() {
605 Some(parent) => match std::fs::create_dir_all(parent) {
606 Ok(()) => Ok(()),
607 Err(source) => Err(Error::Io {
608 action: String::from("create parent directories"),
609 path: Some(parent.to_path_buf()),
610 source,
611 }),
612 },
613 None => Ok(()),
614 }
615}
616
617fn confirm_within(directory: &Path, full: &Path) -> Result<(), Error> {
619 let canonical = match std::fs::canonicalize(full) {
620 Ok(canonical) => canonical,
621 Err(source) => {
622 return Err(Error::Io {
623 action: String::from("resolve the created path"),
624 path: Some(full.to_path_buf()),
625 source,
626 });
627 }
628 };
629 if canonical.starts_with(directory) {
630 Ok(())
631 } else {
632 Err(Error::Escapes {
633 path: full.to_path_buf(),
634 })
635 }
636}