1use super::{ArchiveCounts, ArchiveEvent, BINARIES_METADATA_FILE_NAME, CARGO_METADATA_FILE_NAME};
5use crate::{
6 config::{
7 core::{EvaluatableProfile, get_num_cpus},
8 elements::{ArchiveConfig, ArchiveIncludeOnMissing, RecursionDepth},
9 },
10 errors::{ArchiveCreateError, FromMessagesError, UnknownArchiveFormat, WriteTestListError},
11 helpers::{convert_rel_path_to_forward_slash, rel_path_join},
12 list::{BinaryList, RustBuildMeta, RustTestArtifact},
13 redact::Redactor,
14 reuse_build::{ArchiveFilterCounts, LIBDIRS_BASE_DIR, PathMapper},
15 test_filter::{BinaryFilter, FilterBinaryMatch, FilterBound},
16};
17use atomicwrites::{AtomicFile, OverwriteBehavior};
18use camino::{Utf8Path, Utf8PathBuf};
19use core::fmt;
20use guppy::{PackageId, graph::PackageGraph};
21use nextest_filtering::EvalContext;
22use std::{
23 collections::{BTreeSet, HashSet},
24 fs,
25 io::{self, BufWriter, Write},
26 sync::Arc,
27 time::{Instant, SystemTime},
28};
29use tracing::{debug, trace};
30use zstd::Encoder;
31
32pub fn apply_archive_filters(
34 graph: &PackageGraph,
35 binary_list: Arc<BinaryList>,
36 filter: &BinaryFilter,
37 ecx: &EvalContext<'_>,
38 path_mapper: &PathMapper,
39) -> Result<(BinaryList, ArchiveFilterCounts), FromMessagesError> {
40 let rust_build_meta = binary_list.rust_build_meta.map_paths(path_mapper);
41 let test_artifacts = RustTestArtifact::from_binary_list(
42 graph,
43 binary_list.clone(),
44 &rust_build_meta,
45 path_mapper,
46 None,
47 )?;
48
49 let test_artifacts: BTreeSet<_> = test_artifacts
51 .iter()
52 .filter(|test_artifact| {
53 let query = test_artifact.to_binary_query();
57 let filter_match = filter.check_match(&query, ecx, FilterBound::All);
58
59 debug_assert!(
60 !matches!(filter_match, FilterBinaryMatch::Possible),
61 "build_filtersets should have errored out on test filters, \
62 Possible should never be returned"
63 );
64 matches!(filter_match, FilterBinaryMatch::Definite)
65 })
66 .map(|test_artifact| &test_artifact.binary_id)
67 .collect();
68
69 let filtered_binaries: Vec<_> = binary_list
70 .rust_binaries
71 .iter()
72 .filter(|binary| test_artifacts.contains(&binary.id))
73 .cloned()
74 .collect();
75
76 let relevant_package_ids: HashSet<&str> = filtered_binaries
79 .iter()
80 .map(|binary| binary.package_id.as_str())
81 .collect();
82 let partitioned_non_test_binaries = binary_list
83 .rust_build_meta
84 .non_test_binaries
85 .partition_for_archive(&relevant_package_ids);
86
87 let mut filtered_build_script_out_dirs =
89 binary_list.rust_build_meta.build_script_out_dirs.clone();
90 filtered_build_script_out_dirs
91 .retain(|package_id, _| relevant_package_ids.contains(package_id.as_str()));
92 let filtered_build_script_info =
93 binary_list
94 .rust_build_meta
95 .build_script_info
96 .as_ref()
97 .map(|info| {
98 info.iter()
99 .filter(|(package_id, _)| relevant_package_ids.contains(package_id.as_str()))
100 .map(|(k, v)| (k.clone(), v.clone()))
101 .collect()
102 });
103
104 let filtered_out_test_binary_count = binary_list
105 .rust_binaries
106 .len()
107 .saturating_sub(filtered_binaries.len());
108 let filtered_out_non_test_binary_count =
109 partitioned_non_test_binaries.filtered_out_binary_count;
110 let filtered_out_build_script_out_dir_count = binary_list
111 .rust_build_meta
112 .build_script_out_dirs
113 .len()
114 .saturating_sub(filtered_build_script_out_dirs.len());
115
116 let filtered_build_meta = RustBuildMeta {
117 non_test_binaries: partitioned_non_test_binaries.retained,
118 build_script_out_dirs: filtered_build_script_out_dirs,
119 build_script_info: filtered_build_script_info,
120 ..binary_list.rust_build_meta.clone()
121 };
122
123 Ok((
124 BinaryList {
125 rust_build_meta: filtered_build_meta,
126 rust_binaries: filtered_binaries,
127 },
128 ArchiveFilterCounts {
129 filtered_out_test_binary_count,
130 filtered_out_non_test_binary_count,
131 filtered_out_build_script_out_dir_count,
132 },
133 ))
134}
135
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
138#[non_exhaustive]
139pub enum ArchiveFormat {
140 TarZst,
142}
143
144impl ArchiveFormat {
145 pub const SUPPORTED_FORMATS: &'static [(&'static str, Self)] = &[(".tar.zst", Self::TarZst)];
147
148 pub fn autodetect(archive_file: &Utf8Path) -> Result<Self, UnknownArchiveFormat> {
151 let file_name = archive_file.file_name().unwrap_or("");
152 for (extension, format) in Self::SUPPORTED_FORMATS {
153 if file_name.ends_with(extension) {
154 return Ok(*format);
155 }
156 }
157
158 Err(UnknownArchiveFormat {
159 file_name: file_name.to_owned(),
160 })
161 }
162}
163
164#[expect(clippy::too_many_arguments)]
168pub fn archive_to_file<'a, F>(
169 profile: EvaluatableProfile<'a>,
170 binary_list: &'a BinaryList,
171 filter_counts: ArchiveFilterCounts,
172 cargo_metadata: &'a str,
173 graph: &'a PackageGraph,
174 path_mapper: &'a PathMapper,
175 format: ArchiveFormat,
176 zstd_level: i32,
177 output_file: &'a Utf8Path,
178 mut callback: F,
179 redactor: Redactor,
180) -> Result<(), ArchiveCreateError>
181where
182 F: for<'b> FnMut(ArchiveEvent<'b>) -> io::Result<()>,
183{
184 let config = profile.archive_config();
185
186 let start_time = Instant::now();
187
188 let file = AtomicFile::new(output_file, OverwriteBehavior::AllowOverwrite);
189 let file_count = file
190 .write(|file| {
191 let (host_stdlib, host_stdlib_err) = if let Some(libdir) = binary_list
198 .rust_build_meta
199 .build_platforms
200 .host
201 .libdir
202 .as_path()
203 {
204 split_result(find_std(libdir))
205 } else {
206 (None, None)
207 };
208
209 let (target_stdlib, target_stdlib_err) =
210 if let Some(target) = &binary_list.rust_build_meta.build_platforms.target {
211 if let Some(libdir) = target.libdir.as_path() {
212 split_result(find_std(libdir))
213 } else {
214 (None, None)
215 }
216 } else {
217 (None, None)
218 };
219
220 let stdlib_count = host_stdlib.is_some() as usize + target_stdlib.is_some() as usize;
221
222 let archiver = Archiver::new(
223 config,
224 binary_list,
225 cargo_metadata,
226 graph,
227 path_mapper,
228 host_stdlib,
229 target_stdlib,
230 format,
231 zstd_level,
232 file,
233 redactor,
234 )?;
235
236 let test_binary_count = binary_list.rust_binaries.len();
237 let non_test_binary_count =
238 binary_list.rust_build_meta.non_test_binaries.binary_count();
239 let build_script_out_dir_count =
240 binary_list.rust_build_meta.build_script_out_dirs.len();
241 let linked_path_count = binary_list.rust_build_meta.linked_paths.len();
242 let extra_path_count = config.include.len();
243
244 let counts = ArchiveCounts {
245 test_binary_count,
246 filter_counts,
247 non_test_binary_count,
248 build_script_out_dir_count,
249 linked_path_count,
250 extra_path_count,
251 stdlib_count,
252 };
253
254 callback(ArchiveEvent::ArchiveStarted {
255 counts,
256 output_file,
257 })
258 .map_err(ArchiveCreateError::ReporterIo)?;
259
260 if let Some(err) = host_stdlib_err {
262 callback(ArchiveEvent::StdlibPathError {
263 error: &err.to_string(),
264 })
265 .map_err(ArchiveCreateError::ReporterIo)?;
266 }
267 if let Some(err) = target_stdlib_err {
268 callback(ArchiveEvent::StdlibPathError {
269 error: &err.to_string(),
270 })
271 .map_err(ArchiveCreateError::ReporterIo)?;
272 }
273
274 let (_, file_count) = archiver.archive(&mut callback)?;
275 Ok(file_count)
276 })
277 .map_err(|err| match err {
278 atomicwrites::Error::Internal(err) => ArchiveCreateError::OutputArchiveIo(err),
279 atomicwrites::Error::User(err) => err,
280 })?;
281
282 let elapsed = start_time.elapsed();
283
284 callback(ArchiveEvent::Archived {
285 file_count,
286 output_file,
287 elapsed,
288 })
289 .map_err(ArchiveCreateError::ReporterIo)?;
290
291 Ok(())
292}
293
294struct Archiver<'a, W: Write> {
295 binary_list: &'a BinaryList,
296 cargo_metadata: &'a str,
297 graph: &'a PackageGraph,
298 path_mapper: &'a PathMapper,
299 host_stdlib: Option<Utf8PathBuf>,
300 target_stdlib: Option<Utf8PathBuf>,
301 builder: tar::Builder<Encoder<'static, BufWriter<W>>>,
302 unix_timestamp: u64,
303 added_files: HashSet<Utf8PathBuf>,
304 config: &'a ArchiveConfig,
305 redactor: Redactor,
306}
307
308impl<'a, W: Write> Archiver<'a, W> {
309 #[expect(clippy::too_many_arguments)]
310 fn new(
311 config: &'a ArchiveConfig,
312 binary_list: &'a BinaryList,
313 cargo_metadata: &'a str,
314 graph: &'a PackageGraph,
315 path_mapper: &'a PathMapper,
316 host_stdlib: Option<Utf8PathBuf>,
317 target_stdlib: Option<Utf8PathBuf>,
318 format: ArchiveFormat,
319 compression_level: i32,
320 writer: W,
321 redactor: Redactor,
322 ) -> Result<Self, ArchiveCreateError> {
323 let buf_writer = BufWriter::new(writer);
324 let builder = match format {
325 ArchiveFormat::TarZst => {
326 let mut encoder = zstd::Encoder::new(buf_writer, compression_level)
327 .map_err(ArchiveCreateError::OutputArchiveIo)?;
328 encoder
329 .include_checksum(true)
330 .map_err(ArchiveCreateError::OutputArchiveIo)?;
331 if let Err(err) = encoder.multithread(get_num_cpus() as u32) {
332 tracing::warn!(
333 ?err,
334 "libzstd compiled without multithreading, defaulting to single-thread"
335 );
336 }
337 tar::Builder::new(encoder)
338 }
339 };
340
341 let unix_timestamp = SystemTime::now()
342 .duration_since(SystemTime::UNIX_EPOCH)
343 .expect("current time should be after 1970-01-01")
344 .as_secs();
345
346 Ok(Self {
347 binary_list,
348 cargo_metadata,
349 graph,
350 path_mapper,
351 host_stdlib,
352 target_stdlib,
353 builder,
354 unix_timestamp,
355 added_files: HashSet::new(),
356 config,
357 redactor,
358 })
359 }
360
361 fn archive<F>(mut self, callback: &mut F) -> Result<(W, usize), ArchiveCreateError>
362 where
363 F: for<'b> FnMut(ArchiveEvent<'b>) -> io::Result<()>,
364 {
365 let archive_summary = self.binary_list.to_archive_summary();
369 let binaries_metadata = serde_json::to_string_pretty(&archive_summary)
370 .map_err(|e| ArchiveCreateError::CreateBinaryList(WriteTestListError::Json(e)))?;
371
372 self.append_from_memory(BINARIES_METADATA_FILE_NAME, &binaries_metadata)?;
373
374 self.append_from_memory(CARGO_METADATA_FILE_NAME, self.cargo_metadata)?;
375
376 let target_dir = &self.binary_list.rust_build_meta.target_directory;
377 let build_directory = &self.binary_list.rust_build_meta.build_directory;
378
379 fn filter_map_err<T>(result: io::Result<()>) -> Option<Result<T, ArchiveCreateError>> {
380 match result {
381 Ok(()) => None,
382 Err(err) => Some(Err(ArchiveCreateError::ReporterIo(err))),
383 }
384 }
385
386 let archive_include_paths = self
388 .config
389 .include
390 .iter()
391 .filter_map(|include| {
392 let src_path = include.join_path(target_dir);
393 let src_path = self.path_mapper.map_target_path(src_path);
395
396 match src_path.symlink_metadata() {
397 Ok(metadata) => {
398 if metadata.is_dir() {
399 if include.depth().is_zero() {
400 filter_map_err(callback(ArchiveEvent::DirectoryAtDepthZero {
402 path: &src_path,
403 }))
404 } else {
405 Some(Ok((include, src_path)))
406 }
407 } else if metadata.is_file() || metadata.is_symlink() {
408 Some(Ok((include, src_path)))
409 } else {
410 filter_map_err(callback(ArchiveEvent::UnknownFileType {
411 step: ArchiveStep::ExtraPaths,
412 path: &src_path,
413 }))
414 }
415 }
416 Err(error) => {
417 if error.kind() == io::ErrorKind::NotFound {
418 match include.on_missing() {
419 ArchiveIncludeOnMissing::Error => {
420 Some(Err(ArchiveCreateError::MissingExtraPath {
422 path: src_path.to_owned(),
423 redactor: self.redactor.clone(),
424 }))
425 }
426 ArchiveIncludeOnMissing::Warn => {
427 filter_map_err(callback(ArchiveEvent::ExtraPathMissing {
428 path: &src_path,
429 warn: true,
430 }))
431 }
432 ArchiveIncludeOnMissing::Ignore => {
433 filter_map_err(callback(ArchiveEvent::ExtraPathMissing {
434 path: &src_path,
435 warn: false,
436 }))
437 }
438 }
439 } else {
440 Some(Err(ArchiveCreateError::InputFileRead {
441 step: ArchiveStep::ExtraPaths,
442 path: src_path.to_owned(),
443 is_dir: None,
444 error,
445 }))
446 }
447 }
448 }
449 })
450 .collect::<Result<Vec<_>, ArchiveCreateError>>()?;
451
452 for binary in &self.binary_list.rust_binaries {
455 let rel_path = binary
456 .path
457 .strip_prefix(build_directory)
458 .expect("test binary paths must be within the build directory");
459 let rel_path = Utf8Path::new("target").join(rel_path);
461 let rel_path = convert_rel_path_to_forward_slash(&rel_path);
462
463 self.append_file(ArchiveStep::TestBinaries, &binary.path, &rel_path)?;
464 }
465 for non_test_binary in self.binary_list.rust_build_meta.non_test_binaries.files() {
466 let src_path = self
467 .binary_list
468 .rust_build_meta
469 .target_directory
470 .join(&non_test_binary.path);
471 let src_path = self.path_mapper.map_target_path(src_path);
473
474 let rel_path = Utf8Path::new("target").join(&non_test_binary.path);
475 let rel_path = convert_rel_path_to_forward_slash(&rel_path);
476
477 self.append_file(ArchiveStep::NonTestBinaries, &src_path, &rel_path)?;
478 }
479
480 for build_script_out_dir in self
483 .binary_list
484 .rust_build_meta
485 .build_script_out_dirs
486 .values()
487 {
488 let src_path = build_directory.join(build_script_out_dir);
489 let src_path = self.path_mapper.map_build_path(src_path);
490
491 let rel_path = Utf8Path::new("target").join(build_script_out_dir);
492 let rel_path = convert_rel_path_to_forward_slash(&rel_path);
493
494 self.append_path_recursive(
498 ArchiveStep::BuildScriptOutDirs,
499 &src_path,
500 &rel_path,
501 RecursionDepth::Finite(1),
502 false,
503 callback,
504 )?;
505
506 }
510
511 for (linked_path, requested_by) in &self.binary_list.rust_build_meta.linked_paths {
513 let src_path = build_directory.join(linked_path);
516 let src_path = self.path_mapper.map_build_path(src_path);
517
518 if !src_path.exists() {
520 let mut requested_by: Vec<_> = requested_by
522 .iter()
523 .map(|package_id| {
524 self.graph
525 .metadata(&PackageId::new(package_id.clone()))
526 .map_or_else(
527 |_| {
528 package_id.to_owned()
531 },
532 |metadata| format!("{} v{}", metadata.name(), metadata.version()),
533 )
534 })
535 .collect();
536 requested_by.sort_unstable();
537
538 callback(ArchiveEvent::LinkedPathNotFound {
539 path: &src_path,
540 requested_by: &requested_by,
541 })
542 .map_err(ArchiveCreateError::ReporterIo)?;
543 continue;
544 }
545
546 let rel_path = Utf8Path::new("target").join(linked_path);
547 let rel_path = convert_rel_path_to_forward_slash(&rel_path);
548 self.append_path_recursive(
551 ArchiveStep::LinkedPaths,
552 &src_path,
553 &rel_path,
554 RecursionDepth::Finite(1),
555 false,
556 callback,
557 )?;
558 }
559
560 for (include, src_path) in archive_include_paths {
562 let rel_path = include.join_path(Utf8Path::new("target"));
563 let rel_path = convert_rel_path_to_forward_slash(&rel_path);
564
565 if src_path.exists() {
566 self.append_path_recursive(
567 ArchiveStep::ExtraPaths,
568 &src_path,
569 &rel_path,
570 include.depth(),
571 true,
573 callback,
574 )?;
575 }
576 }
577
578 if let Some(host_stdlib) = self.host_stdlib.clone() {
580 let rel_path = Utf8Path::new(LIBDIRS_BASE_DIR)
581 .join("host")
582 .join(host_stdlib.file_name().unwrap());
583 let rel_path = convert_rel_path_to_forward_slash(&rel_path);
584
585 self.append_file(ArchiveStep::ExtraPaths, &host_stdlib, &rel_path)?;
586 }
587 if let Some(target_stdlib) = self.target_stdlib.clone() {
588 let rel_path = Utf8Path::new(LIBDIRS_BASE_DIR)
591 .join("target/0")
592 .join(target_stdlib.file_name().unwrap());
593 let rel_path = convert_rel_path_to_forward_slash(&rel_path);
594
595 self.append_file(ArchiveStep::ExtraPaths, &target_stdlib, &rel_path)?;
596 }
597
598 let encoder = self
600 .builder
601 .into_inner()
602 .map_err(ArchiveCreateError::OutputArchiveIo)?;
603 let buf_writer = encoder
605 .finish()
606 .map_err(ArchiveCreateError::OutputArchiveIo)?;
607 let writer = buf_writer
608 .into_inner()
609 .map_err(|err| ArchiveCreateError::OutputArchiveIo(err.into_error()))?;
610
611 Ok((writer, self.added_files.len()))
612 }
613
614 fn append_from_memory(&mut self, name: &str, contents: &str) -> Result<(), ArchiveCreateError> {
619 let mut header = tar::Header::new_gnu();
620 header.set_size(contents.len() as u64);
621 header.set_mtime(self.unix_timestamp);
622 header.set_mode(0o664);
623 header.set_cksum();
624
625 self.builder
626 .append_data(&mut header, name, io::Cursor::new(contents))
627 .map_err(ArchiveCreateError::OutputArchiveIo)?;
628 self.added_files.insert(name.into());
631 Ok(())
632 }
633
634 fn append_path_recursive<F>(
635 &mut self,
636 step: ArchiveStep,
637 src_path: &Utf8Path,
638 rel_path: &Utf8Path,
639 limit: RecursionDepth,
640 warn_on_exceed_depth: bool,
641 callback: &mut F,
642 ) -> Result<(), ArchiveCreateError>
643 where
644 F: for<'b> FnMut(ArchiveEvent<'b>) -> io::Result<()>,
645 {
646 let metadata =
648 fs::symlink_metadata(src_path).map_err(|error| ArchiveCreateError::InputFileRead {
649 step,
650 path: src_path.to_owned(),
651 is_dir: None,
652 error,
653 })?;
654
655 let mut stack = vec![(limit, src_path.to_owned(), rel_path.to_owned(), metadata)];
657
658 while let Some((depth, src_path, rel_path, metadata)) = stack.pop() {
659 trace!(
660 target: "nextest-runner",
661 "processing `{src_path}` with metadata {metadata:?} \
662 (depth: {depth})",
663 );
664
665 if metadata.is_dir() {
666 if depth.is_zero() {
668 callback(ArchiveEvent::RecursionDepthExceeded {
669 step,
670 path: &src_path,
671 limit: limit.unwrap_finite(),
672 warn: warn_on_exceed_depth,
673 })
674 .map_err(ArchiveCreateError::ReporterIo)?;
675 continue;
676 }
677
678 debug!(
680 target: "nextest-runner",
681 "recursing into `{}`",
682 src_path
683 );
684 let entries = src_path.read_dir_utf8().map_err(|error| {
685 ArchiveCreateError::InputFileRead {
686 step,
687 path: src_path.to_owned(),
688 is_dir: Some(true),
689 error,
690 }
691 })?;
692 for entry in entries {
693 let entry = entry.map_err(|error| ArchiveCreateError::DirEntryRead {
694 path: src_path.to_owned(),
695 error,
696 })?;
697 let metadata =
698 entry
699 .metadata()
700 .map_err(|error| ArchiveCreateError::InputFileRead {
701 step,
702 path: entry.path().to_owned(),
703 is_dir: None,
704 error,
705 })?;
706 let entry_rel_path = rel_path_join(&rel_path, entry.file_name().as_ref());
707 stack.push((
708 depth.decrement(),
709 entry.into_path(),
710 entry_rel_path,
711 metadata,
712 ));
713 }
714 } else if metadata.is_file() || metadata.is_symlink() {
715 self.append_file(step, &src_path, &rel_path)?;
716 } else {
717 callback(ArchiveEvent::UnknownFileType {
719 step,
720 path: &src_path,
721 })
722 .map_err(ArchiveCreateError::ReporterIo)?;
723 }
724 }
725
726 Ok(())
727 }
728
729 fn append_file(
730 &mut self,
731 step: ArchiveStep,
732 src: &Utf8Path,
733 dest: &Utf8Path,
734 ) -> Result<(), ArchiveCreateError> {
735 if !self.added_files.contains(dest) {
737 debug!(
738 target: "nextest-runner",
739 "adding `{src}` to archive as `{dest}`",
740 );
741 self.builder
742 .append_path_with_name(src, dest)
743 .map_err(|error| ArchiveCreateError::InputFileRead {
744 step,
745 path: src.to_owned(),
746 is_dir: Some(false),
747 error,
748 })?;
749 self.added_files.insert(dest.into());
750 }
751 Ok(())
752 }
753}
754
755fn find_std(libdir: &Utf8Path) -> io::Result<Utf8PathBuf> {
756 for path in libdir.read_dir_utf8()? {
757 let path = path?;
758 let file_name = path.file_name();
764 let is_unix = file_name.starts_with("libstd-")
765 && (file_name.ends_with(".so") || file_name.ends_with(".dylib"));
766 let is_windows = file_name.starts_with("std-") && file_name.ends_with(".dll");
767
768 if is_unix || is_windows {
769 return Ok(path.into_path());
770 }
771 }
772
773 Err(io::Error::other(
774 "could not find the Rust standard library in the libdir",
775 ))
776}
777
778fn split_result<T, E>(result: Result<T, E>) -> (Option<T>, Option<E>) {
779 match result {
780 Ok(v) => (Some(v), None),
781 Err(e) => (None, Some(e)),
782 }
783}
784
785#[derive(Clone, Copy, Debug)]
789pub enum ArchiveStep {
790 TestBinaries,
792
793 NonTestBinaries,
795
796 BuildScriptOutDirs,
798
799 LinkedPaths,
801
802 ExtraPaths,
804
805 Stdlib,
807}
808
809impl fmt::Display for ArchiveStep {
810 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
811 match self {
812 Self::TestBinaries => write!(f, "test binaries"),
813 Self::NonTestBinaries => write!(f, "non-test binaries"),
814 Self::BuildScriptOutDirs => write!(f, "build script output directories"),
815 Self::LinkedPaths => write!(f, "linked paths"),
816 Self::ExtraPaths => write!(f, "extra paths"),
817 Self::Stdlib => write!(f, "standard library"),
818 }
819 }
820}
821
822#[cfg(test)]
823mod tests {
824 use super::*;
825
826 #[test]
827 fn test_archive_format_autodetect() {
828 assert_eq!(
829 ArchiveFormat::autodetect("foo.tar.zst".as_ref()).unwrap(),
830 ArchiveFormat::TarZst,
831 );
832 assert_eq!(
833 ArchiveFormat::autodetect("foo/bar.tar.zst".as_ref()).unwrap(),
834 ArchiveFormat::TarZst,
835 );
836 ArchiveFormat::autodetect("foo".as_ref()).unwrap_err();
837 ArchiveFormat::autodetect("/".as_ref()).unwrap_err();
838 }
839}