@@ -76,10 +76,53 @@ pub enum Error {
7676 #[ error( "could not extract source archive: {0}" ) ]
7777 ArchiveExtract ( std:: io:: Error ) ,
7878
79+ #[ error( "could not extract source archive: {0}" ) ]
80+ ZipExtract ( zip:: result:: ZipError ) ,
81+
7982 #[ error( transparent) ]
8083 Data ( #[ from] data:: Error ) ,
8184}
8285
86+ /// Container formats we can extract a source tree from. This only concerns how
87+ /// the tree is packed for transport; the tree itself is always wrapped in a
88+ /// single top-level directory (SEP-58), which callers check after extraction.
89+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
90+ pub ( crate ) enum ArchiveFormat {
91+ /// Gzipped tarball — what `build --verifiable` produces.
92+ TarGz ,
93+ /// Zip archive.
94+ Zip ,
95+ }
96+
97+ /// Recognized archive extensions and the format each maps to, matched
98+ /// case-insensitively as a suffix of the archive's filename. Single source of
99+ /// truth for both format detection and the "accepted formats" error text.
100+ const ARCHIVE_EXTENSIONS : & [ ( & str , ArchiveFormat ) ] = & [
101+ ( ".tar.gz" , ArchiveFormat :: TarGz ) ,
102+ ( ".tgz" , ArchiveFormat :: TarGz ) ,
103+ ( ".zip" , ArchiveFormat :: Zip ) ,
104+ ] ;
105+
106+ impl ArchiveFormat {
107+ /// The format named by `filename`'s extension, or `None` if unrecognized.
108+ pub ( crate ) fn from_filename ( filename : & str ) -> Option < Self > {
109+ let lower = filename. to_ascii_lowercase ( ) ;
110+ ARCHIVE_EXTENSIONS
111+ . iter ( )
112+ . find ( |( ext, _) | lower. ends_with ( ext) )
113+ . map ( |( _, format) | * format)
114+ }
115+
116+ /// Comma-separated list of accepted extensions, for error messages.
117+ pub ( crate ) fn recognized_extensions ( ) -> String {
118+ ARCHIVE_EXTENSIONS
119+ . iter ( )
120+ . map ( |( ext, _) | * ext)
121+ . collect :: < Vec < _ > > ( )
122+ . join ( ", " )
123+ }
124+ }
125+
83126/// The source tree's root: always the current working directory. The archive is
84127/// rooted there as-is — we do NOT search upward for a git repository or anchor on
85128/// `--manifest-path`'s directory, since for a workspace member the build needs
@@ -305,11 +348,20 @@ pub(crate) fn unpack_targz(bytes: &[u8], dest: &Path) -> Result<(), Error> {
305348 . map_err ( Error :: ArchiveExtract )
306349}
307350
308- /// Create a fresh temp directory, unpack the gzipped source tarball `bytes` into
309- /// it, harden its permissions, and return the guard (the tree lives at its
310- /// `path()`). Shared by `build --verifiable` (builds from the extracted copy)
311- /// and `verify` (rebuilds from it); `prefix` names the dir so the two are
312- /// distinguishable on disk.
351+ /// Unpack a zip archive into `dest`. `ZipArchive::extract` sanitizes each
352+ /// entry's path (dropping anything that would escape `dest`), so a hostile
353+ /// archive can't write outside the tempdir.
354+ pub ( crate ) fn unpack_zip ( bytes : & [ u8 ] , dest : & Path ) -> Result < ( ) , Error > {
355+ zip:: ZipArchive :: new ( std:: io:: Cursor :: new ( bytes) )
356+ . and_then ( |mut archive| archive. extract ( dest) )
357+ . map_err ( Error :: ZipExtract )
358+ }
359+
360+ /// Create a fresh temp directory, unpack the source archive `bytes` (in the
361+ /// given `format`) into it, harden its permissions, and return the guard (the
362+ /// tree lives at its `path()`). Shared by `build --verifiable` (builds from the
363+ /// extracted copy) and `verify` (rebuilds from it); `prefix` names the dir so
364+ /// the two are distinguishable on disk.
313365///
314366/// The temp dir is created under `<data dir>/tmp`, NOT the OS temp dir: on macOS
315367/// `$TMPDIR` lives under /var/folders, which container VMs (Docker Desktop,
@@ -321,6 +373,7 @@ pub(crate) fn unpack_targz(bytes: &[u8], dest: &Path) -> Result<(), Error> {
321373pub ( crate ) fn extract_into_hardened_tempdir (
322374 bytes : & [ u8 ] ,
323375 prefix : & str ,
376+ format : ArchiveFormat ,
324377) -> Result < tempfile:: TempDir , Error > {
325378 let base = data:: data_local_dir ( ) ?. join ( "tmp" ) ;
326379 std:: fs:: create_dir_all ( & base) . map_err ( |source| Error :: ArchiveWrite {
@@ -331,7 +384,10 @@ pub(crate) fn extract_into_hardened_tempdir(
331384 . prefix ( prefix)
332385 . tempdir_in ( & base)
333386 . map_err ( Error :: ArchiveExtract ) ?;
334- unpack_targz ( bytes, tmp. path ( ) ) ?;
387+ match format {
388+ ArchiveFormat :: TarGz => unpack_targz ( bytes, tmp. path ( ) ) ?,
389+ ArchiveFormat :: Zip => unpack_zip ( bytes, tmp. path ( ) ) ?,
390+ }
335391 enforce_hardened_tree ( tmp. path ( ) ) . map_err ( Error :: ArchiveExtract ) ?;
336392 Ok ( tmp)
337393}
@@ -342,6 +398,56 @@ mod tests {
342398 use crate :: config:: locator:: enforce_hardened_tree;
343399 use sha2:: { Digest , Sha256 } ;
344400
401+ #[ test]
402+ fn archive_format_from_filename ( ) {
403+ assert_eq ! (
404+ ArchiveFormat :: from_filename( "src.tar.gz" ) ,
405+ Some ( ArchiveFormat :: TarGz )
406+ ) ;
407+ assert_eq ! (
408+ ArchiveFormat :: from_filename( "SRC.TGZ" ) ,
409+ Some ( ArchiveFormat :: TarGz )
410+ ) ;
411+ assert_eq ! (
412+ ArchiveFormat :: from_filename( "src.zip" ) ,
413+ Some ( ArchiveFormat :: Zip )
414+ ) ;
415+ assert_eq ! ( ArchiveFormat :: from_filename( "src.rar" ) , None ) ;
416+ assert_eq ! ( ArchiveFormat :: from_filename( "src" ) , None ) ;
417+ // The listed extensions are exactly what the error surfaces.
418+ assert_eq ! (
419+ ArchiveFormat :: recognized_extensions( ) ,
420+ ".tar.gz, .tgz, .zip"
421+ ) ;
422+ }
423+
424+ #[ test]
425+ fn unpack_zip_round_trips ( ) {
426+ use std:: io:: Write ;
427+ // Build a small zip with a single top-level `source/` dir.
428+ let mut buf = Vec :: new ( ) ;
429+ {
430+ let mut zip = zip:: ZipWriter :: new ( std:: io:: Cursor :: new ( & mut buf) ) ;
431+ let opts: zip:: write:: FileOptions < ' _ , ( ) > = zip:: write:: FileOptions :: default ( ) ;
432+ zip. start_file ( "source/Cargo.toml" , opts) . unwrap ( ) ;
433+ zip. write_all ( b"# crate" ) . unwrap ( ) ;
434+ zip. start_file ( "source/src/lib.rs" , opts) . unwrap ( ) ;
435+ zip. write_all ( b"// code" ) . unwrap ( ) ;
436+ zip. finish ( ) . unwrap ( ) ;
437+ }
438+
439+ let dest = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
440+ unpack_zip ( & buf, dest. path ( ) ) . unwrap ( ) ;
441+ assert_eq ! (
442+ std:: fs:: read( dest. path( ) . join( "source/Cargo.toml" ) ) . unwrap( ) ,
443+ b"# crate"
444+ ) ;
445+ assert_eq ! (
446+ std:: fs:: read( dest. path( ) . join( "source/src/lib.rs" ) ) . unwrap( ) ,
447+ b"// code"
448+ ) ;
449+ }
450+
345451 #[ test]
346452 fn is_warned_matches_names_and_dotted_suffixes ( ) {
347453 use std:: ffi:: OsStr ;
0 commit comments