@@ -648,6 +648,98 @@ fn extraction_io(action: &str, path: &Path, error: &io::Error) -> CliError {
648648 CliError :: io ( format ! ( "{action} {}: {error}" , path. display( ) ) )
649649}
650650
651+ fn is_known_immutable_root_alias ( name : & OsStr ) -> bool {
652+ #[ cfg( target_os = "macos" ) ]
653+ {
654+ name == OsStr :: new ( "var" ) || name == OsStr :: new ( "tmp" )
655+ }
656+ #[ cfg( not( target_os = "macos" ) ) ]
657+ {
658+ let _ = name;
659+ false
660+ }
661+ }
662+
663+ fn open_or_create_output_component (
664+ parent : & Dir ,
665+ name : & OsStr ,
666+ output_dir : & Path ,
667+ allow_root_alias : bool ,
668+ ) -> Result < Dir , CliError > {
669+ match parent. open_dir_nofollow ( name) {
670+ Ok ( directory) => return Ok ( directory) ,
671+ Err ( error) if error. kind ( ) != io:: ErrorKind :: NotFound => {
672+ if allow_root_alias
673+ && is_known_immutable_root_alias ( name)
674+ && parent
675+ . symlink_metadata ( name)
676+ . is_ok_and ( |metadata| metadata. file_type ( ) . is_symlink ( ) )
677+ {
678+ // These macOS root aliases are not mutable by ordinary users.
679+ return parent. open_dir ( name) . map_err ( |open_error| {
680+ extraction_io ( "failed to open output directory" , output_dir, & open_error)
681+ } ) ;
682+ }
683+ return Err ( extraction_io ( "failed to open output directory" , output_dir, & error) ) ;
684+ }
685+ Err ( _) => { }
686+ }
687+
688+ match parent. create_dir ( name) {
689+ Ok ( ( ) ) => { }
690+ Err ( error) if error. kind ( ) == io:: ErrorKind :: AlreadyExists => { }
691+ Err ( error) => {
692+ return Err ( extraction_io ( "failed to create output directory" , output_dir, & error) ) ;
693+ }
694+ }
695+
696+ parent
697+ . open_dir_nofollow ( name)
698+ . map_err ( |error| extraction_io ( "failed to open output directory" , output_dir, & error) )
699+ }
700+
701+ fn open_or_create_output_dir ( output_dir : & Path ) -> Result < Dir , CliError > {
702+ let absolute = std:: path:: absolute ( output_dir)
703+ . map_err ( |error| extraction_io ( "failed to resolve output directory" , output_dir, & error) ) ?;
704+ let root_path = absolute. ancestors ( ) . last ( ) . ok_or_else ( || {
705+ CliError :: io ( format ! ( "output directory has no root: {}" , output_dir. display( ) ) )
706+ } ) ?;
707+ let relative = absolute. strip_prefix ( root_path) . map_err ( |error| {
708+ CliError :: io ( format ! (
709+ "failed to resolve output directory {}: {error}" ,
710+ output_dir. display( )
711+ ) )
712+ } ) ?;
713+ let mut components = Vec :: new ( ) ;
714+ for component in relative. components ( ) {
715+ match component {
716+ Component :: Normal ( name) => components. push ( name. to_os_string ( ) ) ,
717+ Component :: CurDir => { }
718+ Component :: ParentDir => {
719+ components. pop ( ) . ok_or_else ( || {
720+ CliError :: path_traversal ( format ! (
721+ "output directory escapes its filesystem root: {}" ,
722+ output_dir. display( )
723+ ) )
724+ } ) ?;
725+ }
726+ Component :: Prefix ( _) | Component :: RootDir => {
727+ return Err ( CliError :: invalid_input ( format ! (
728+ "output directory contains an unexpected root component: {}" ,
729+ output_dir. display( )
730+ ) ) ) ;
731+ }
732+ }
733+ }
734+
735+ let mut current = Dir :: open_ambient_dir ( root_path, ambient_authority ( ) )
736+ . map_err ( |error| extraction_io ( "failed to open output directory" , output_dir, & error) ) ?;
737+ for ( index, component) in components. iter ( ) . enumerate ( ) {
738+ current = open_or_create_output_component ( & current, component, output_dir, index == 0 ) ?;
739+ }
740+ Ok ( current)
741+ }
742+
651743fn parent_entry_is_unsafe ( parent : & Dir , name : & OsStr ) -> Result < bool , CliError > {
652744 match parent. symlink_metadata ( name) {
653745 Ok ( metadata) => Ok ( metadata. file_type ( ) . is_symlink ( ) || !metadata. is_dir ( ) ) ,
@@ -1783,21 +1875,15 @@ fn parse_fetch_url(input: &str) -> Result<Url, CliError> {
17831875 Ok ( url)
17841876}
17851877
1786- fn fetch_output_dir ( output : & Option < PathBuf > ) -> Result < PathBuf , CliError > {
1878+ fn fetch_output_dir ( output : & Option < PathBuf > ) -> Result < ( PathBuf , Dir ) , CliError > {
17871879 let output_dir = match output {
17881880 Some ( dir) => dir. clone ( ) ,
17891881 None => {
17901882 std:: env:: current_dir ( ) . map_err ( |e| CliError :: io ( format ! ( "cannot get cwd: {e}" ) ) ) ?
17911883 }
17921884 } ;
1793-
1794- if !output_dir. exists ( ) {
1795- fs:: create_dir_all ( & output_dir) . map_err ( |e| {
1796- CliError :: io ( format ! ( "failed to create output directory {}: {e}" , output_dir. display( ) ) )
1797- } ) ?;
1798- }
1799-
1800- Ok ( output_dir)
1885+ let output_handle = open_or_create_output_dir ( & output_dir) ?;
1886+ Ok ( ( output_dir, output_handle) )
18011887}
18021888
18031889fn write_fetch_output (
@@ -1968,14 +2054,7 @@ fn cmd_fetch(
19682054 let url = parse_fetch_url ( url) ?;
19692055 let agent = http_agent ( ) ;
19702056
1971- let output_dir = fetch_output_dir ( output) ?;
1972- let output_handle =
1973- Dir :: open_ambient_dir ( & output_dir, ambient_authority ( ) ) . map_err ( |error| {
1974- CliError :: io ( format ! (
1975- "failed to open output directory {}: {error}" ,
1976- output_dir. display( )
1977- ) )
1978- } ) ?;
2057+ let ( output_dir, output_handle) = fetch_output_dir ( output) ?;
19792058 let ( bundle_filename, bundle_path, bundle_body, bundle_url) =
19802059 fetch_bundle ( & agent, & url, & output_handle, & output_dir, allow_cross_origin) ?;
19812060
@@ -2020,10 +2099,7 @@ fn extract_source_contents(
20202099 sm : & SourceMap ,
20212100 output_dir : & Path ,
20222101) -> Result < ( Vec < serde_json:: Value > , Vec < String > ) , CliError > {
2023- Dir :: create_ambient_dir_all ( output_dir, ambient_authority ( ) )
2024- . map_err ( |error| extraction_io ( "failed to create output directory" , output_dir, & error) ) ?;
2025- let root = Dir :: open_ambient_dir ( output_dir, ambient_authority ( ) )
2026- . map_err ( |error| extraction_io ( "failed to open output directory" , output_dir, & error) ) ?;
2102+ let root = open_or_create_output_dir ( output_dir) ?;
20272103 let mut extracted = Vec :: new ( ) ;
20282104 let mut skipped = Vec :: new ( ) ;
20292105
0 commit comments