@@ -7,6 +7,7 @@ use regex::Regex;
77use sha2:: { Digest , Sha256 } ;
88use soroban_spec_tools:: contract:: Spec ;
99use stellar_xdr:: { Hash , ScMetaEntry , ScMetaV0 } ;
10+ use url:: Url ;
1011use walkdir:: WalkDir ;
1112
1213use crate :: {
@@ -121,6 +122,9 @@ pub enum Error {
121122 #[ error( "reading stdin: {0}" ) ]
122123 Stdin ( std:: io:: Error ) ,
123124
125+ #[ error( "source {uri:?} has an unsupported format; accepted formats are {formats}" ) ]
126+ UnsupportedSourceFormat { uri : String , formats : String } ,
127+
124128 #[ error( "the WASM records only `source_sha256` (no `source_uri`). Pass `--source-uri URL_OR_PATH` to provide retrieval." ) ]
125129 SourceUriRequired ,
126130
@@ -588,6 +592,8 @@ async fn materialize_source(
588592 return Err ( Error :: SourceUriRequired ) ;
589593 } ;
590594
595+ validate_source_format ( & source) ?;
596+
591597 print. infoln ( format ! ( "Fetching source code from {source}" ) ) ;
592598 let bytes = fetch_tarball_bytes ( & source) . await ?;
593599 if let Some ( expected) = & meta. source_sha256 {
@@ -600,6 +606,45 @@ async fn materialize_source(
600606 ) ?)
601607}
602608
609+ /// Extensions we accept for a source archive: the archive is always a gzipped
610+ /// tarball (see `source_archive`), so only these name it. Checked
611+ /// case-insensitively against the source's basename.
612+ const RECOGNIZED_SOURCE_EXTENSIONS : & [ & str ] = & [ ".tar.gz" , ".tgz" ] ;
613+
614+ /// The last path segment of `source`, whether it's a URL or a local path. Try
615+ /// parsing as a URL first (so query strings and fragments are dropped); if that
616+ /// fails, `source` is a local path, so fall back to `Path::file_name`.
617+ fn source_basename ( source : & str ) -> String {
618+ if let Ok ( url) = Url :: parse ( source) {
619+ return url
620+ . path_segments ( )
621+ . and_then ( |mut segments| segments. next_back ( ) )
622+ . unwrap_or_default ( )
623+ . to_string ( ) ;
624+ }
625+ Path :: new ( source)
626+ . file_name ( )
627+ . map ( |name| name. to_string_lossy ( ) . into_owned ( ) )
628+ . unwrap_or_default ( )
629+ }
630+
631+ /// Reject a `--source-uri` (or recorded `source_uri`) whose basename doesn't end
632+ /// in a recognized archive extension, before we bother fetching it, naming the
633+ /// formats we accept.
634+ fn validate_source_format ( source : & str ) -> Result < ( ) , Error > {
635+ let basename = source_basename ( source) . to_ascii_lowercase ( ) ;
636+ if RECOGNIZED_SOURCE_EXTENSIONS
637+ . iter ( )
638+ . any ( |ext| basename. ends_with ( ext) )
639+ {
640+ return Ok ( ( ) ) ;
641+ }
642+ Err ( Error :: UnsupportedSourceFormat {
643+ uri : source. to_string ( ) ,
644+ formats : RECOGNIZED_SOURCE_EXTENSIONS . join ( ", " ) ,
645+ } )
646+ }
647+
603648/// Retrieve the tarball bytes. `source` is either an `http(s)://` URL or a
604649/// local file path. The split is by prefix, not by attempting both — keeps
605650/// behavior predictable.
@@ -1307,4 +1352,41 @@ mod tests {
13071352 let p = find_rebuilt_wasm ( dir. path ( ) , & meta, & preexisting) . unwrap ( ) ;
13081353 assert ! ( p. ends_with( "hello.wasm" ) ) ;
13091354 }
1355+
1356+ #[ test]
1357+ fn source_basename_strips_url_query_and_fragment ( ) {
1358+ assert_eq ! (
1359+ source_basename( "https://example.com/path/src.tar.gz?token=abc#frag" ) ,
1360+ "src.tar.gz"
1361+ ) ;
1362+ assert_eq ! ( source_basename( "https://example.com/a/b/x.tgz" ) , "x.tgz" ) ;
1363+ }
1364+
1365+ #[ test]
1366+ fn source_basename_handles_local_paths ( ) {
1367+ assert_eq ! ( source_basename( "/tmp/foo/src.tar.gz" ) , "src.tar.gz" ) ;
1368+ assert_eq ! ( source_basename( "./relative/src.tgz" ) , "src.tgz" ) ;
1369+ assert_eq ! ( source_basename( "src.tar.gz" ) , "src.tar.gz" ) ;
1370+ }
1371+
1372+ #[ test]
1373+ fn validate_source_format_accepts_recognized_extensions ( ) {
1374+ validate_source_format ( "https://example.com/src.tar.gz" ) . unwrap ( ) ;
1375+ validate_source_format ( "/tmp/src.tgz" ) . unwrap ( ) ;
1376+ // Case-insensitive.
1377+ validate_source_format ( "SRC.TAR.GZ" ) . unwrap ( ) ;
1378+ }
1379+
1380+ #[ test]
1381+ fn validate_source_format_rejects_unknown_formats ( ) {
1382+ for source in [
1383+ "https://example.com/src.zip" ,
1384+ "/tmp/src.rar" ,
1385+ "src" ,
1386+ "src.gz" ,
1387+ ] {
1388+ let err = validate_source_format ( source) . unwrap_err ( ) ;
1389+ assert ! ( matches!( err, Error :: UnsupportedSourceFormat { .. } ) ) ;
1390+ }
1391+ }
13101392}
0 commit comments