33
44use flate2:: read:: MultiGzDecoder ;
55use sha2:: { Digest , Sha256 } ;
6+ use std:: ffi:: { OsStr , OsString } ;
67use std:: fs;
78use std:: fs:: File ;
89#[ cfg( test) ]
@@ -370,36 +371,24 @@ pub fn set_rootfs_image_file_mode(
370371/// Replay the ext4 journal and repair automatically correctable filesystem
371372/// state before the driver mutates a preserved guest disk offline.
372373pub fn recover_rootfs_image ( image_path : & Path ) -> Result < ( ) , String > {
373- let mut failures = Vec :: new ( ) ;
374- let mut unavailable = Vec :: new ( ) ;
375-
376- for candidate in e2fs_tool_candidates ( "e2fsck" ) {
377- let label = candidate. display ( ) . to_string ( ) ;
378- match Command :: new ( & candidate)
379- . arg ( "-p" )
380- . arg ( "-f" )
381- . arg ( image_path)
382- . output ( )
383- {
384- Ok ( output) if matches ! ( output. status. code( ) , Some ( 0 ..=2 ) ) => return Ok ( ( ) ) ,
385- Ok ( output) => failures. push ( format ! (
386- "{label} failed with status {}\n stdout: {}\n stderr: {}" ,
387- output. status,
388- String :: from_utf8_lossy( & output. stdout) ,
389- String :: from_utf8_lossy( & output. stderr)
390- ) ) ,
391- Err ( error) if error. kind ( ) == std:: io:: ErrorKind :: NotFound => {
392- unavailable. push ( format ! ( "{label} not found" ) ) ;
393- }
394- Err ( error) => failures. push ( format ! ( "run {label}: {error}" ) ) ,
374+ let output = e2fs_command ( "e2fsck" ) ?
375+ . arg ( "-p" )
376+ . arg ( "-f" )
377+ . arg ( image_path)
378+ . output ( ) ;
379+ match output {
380+ Ok ( output) if matches ! ( output. status. code( ) , Some ( 0 ..=2 ) ) => Ok ( ( ) ) ,
381+ Ok ( output) => Err ( format ! (
382+ "e2fsck failed with status {}\n stdout: {}\n stderr: {}" ,
383+ output. status,
384+ String :: from_utf8_lossy( & output. stdout) ,
385+ String :: from_utf8_lossy( & output. stderr)
386+ ) ) ,
387+ Err ( error) if error. kind ( ) == std:: io:: ErrorKind :: NotFound => {
388+ Err ( "e2fsck not found" . to_string ( ) )
395389 }
390+ Err ( error) => Err ( format ! ( "run e2fsck: {error}" ) ) ,
396391 }
397-
398- Err ( if failures. is_empty ( ) {
399- unavailable. join ( "\n " )
400- } else {
401- failures. join ( "\n " )
402- } )
403392}
404393
405394#[ cfg( target_os = "macos" ) ]
@@ -675,12 +664,14 @@ enum FormatterAttempt {
675664}
676665
677666fn format_ext4_image_from_dir ( source : & Path , image_path : & Path ) -> Result < ( ) , String > {
678- let candidates = [ "mke2fs" , "mkfs.ext4" ]
679- . into_iter ( )
680- . flat_map ( e2fs_tool_candidates) ;
667+ let candidates = [ "mke2fs" , "mkfs.ext4" ] . map ( PathBuf :: from) ;
681668 run_ext4_formatter_candidates ( candidates, |candidate| {
682669 let label = candidate. display ( ) . to_string ( ) ;
683- let output = Command :: new ( candidate)
670+ let mut command = match e2fs_command ( candidate. as_os_str ( ) ) {
671+ Ok ( command) => command,
672+ Err ( error) => return FormatterAttempt :: Failed ( error) ,
673+ } ;
674+ let output = command
684675 . arg ( "-q" )
685676 . arg ( "-F" )
686677 . arg ( "-t" )
@@ -699,10 +690,10 @@ fn format_ext4_image_from_dir(source: &Path, image_path: &Path) -> Result<(), St
699690 String :: from_utf8_lossy( & output. stdout) ,
700691 String :: from_utf8_lossy( & output. stderr)
701692 ) ) ,
702- Err ( err ) if err . kind ( ) == std:: io:: ErrorKind :: NotFound => {
693+ Err ( error ) if error . kind ( ) == std:: io:: ErrorKind :: NotFound => {
703694 FormatterAttempt :: Unavailable ( format ! ( "{label} not found" ) )
704695 }
705- Err ( err ) => FormatterAttempt :: Failed ( format ! ( "run {label}: {err }" ) ) ,
696+ Err ( error ) => FormatterAttempt :: Failed ( format ! ( "run {label}: {error }" ) ) ,
706697 }
707698 } )
708699 . map_err ( |details| {
@@ -895,45 +886,33 @@ fn sandbox_guest_user_ids_from_image_path(
895886 let quoted_path = debugfs_quote_absolute_path ( guest_path)
896887 . expect ( "the static passwd path is a valid debugfs path" ) ;
897888 let command = format ! ( "cat {quoted_path}" ) ;
898- let mut last_error = None ;
899-
900- for candidate in e2fs_tool_candidates ( "debugfs" ) {
901- let label = candidate. display ( ) . to_string ( ) ;
902- match Command :: new ( & candidate)
903- . arg ( "-R" )
904- . arg ( & command)
905- . arg ( image_path)
906- . output ( )
907- {
908- Ok ( output) if output. status . success ( ) => {
909- let passwd = String :: from_utf8 ( output. stdout ) . map_err ( |error| {
910- format ! (
911- "read {guest_path} from {} as UTF-8: {error}" ,
912- image_path. display( )
913- )
914- } ) ?;
915- return parse_sandbox_guest_user_ids ( & passwd, & image_path. display ( ) . to_string ( ) ) ;
916- }
917- Ok ( output) => {
918- last_error = Some ( format ! (
919- "{label} failed with status {}\n stdout: {}\n stderr: {}" ,
920- output. status,
921- String :: from_utf8_lossy( & output. stdout) ,
922- String :: from_utf8_lossy( & output. stderr)
923- ) ) ;
924- }
925- Err ( error) if error. kind ( ) == std:: io:: ErrorKind :: NotFound => {
926- last_error = Some ( format ! ( "{label} not found" ) ) ;
927- }
928- Err ( error) => last_error = Some ( format ! ( "run {label}: {error}" ) ) ,
889+ let output = e2fs_command ( "debugfs" ) ?
890+ . arg ( "-R" )
891+ . arg ( & command)
892+ . arg ( image_path)
893+ . output ( ) ;
894+ match output {
895+ Ok ( output) if output. status . success ( ) => {
896+ let passwd = String :: from_utf8 ( output. stdout ) . map_err ( |error| {
897+ format ! (
898+ "read {guest_path} from {} as UTF-8: {error}" ,
899+ image_path. display( )
900+ )
901+ } ) ?;
902+ parse_sandbox_guest_user_ids ( & passwd, & image_path. display ( ) . to_string ( ) )
929903 }
904+ Ok ( output) => Err ( format ! (
905+ "debugfs command '{command}' failed for {}: debugfs failed with status {}\n stdout: {}\n stderr: {}. Install e2fsprogs (debugfs) and retry" ,
906+ image_path. display( ) ,
907+ output. status,
908+ String :: from_utf8_lossy( & output. stdout) ,
909+ String :: from_utf8_lossy( & output. stderr)
910+ ) ) ,
911+ Err ( error) => Err ( format ! (
912+ "debugfs command '{command}' failed for {}: {error}. Install e2fsprogs (debugfs) and retry" ,
913+ image_path. display( )
914+ ) ) ,
930915 }
931-
932- Err ( format ! (
933- "debugfs command '{command}' failed for {}: {}. Install e2fsprogs (debugfs) and retry" ,
934- image_path. display( ) ,
935- last_error. unwrap_or_else( || "debugfs not found" . to_string( ) )
936- ) )
937916}
938917
939918fn sandbox_guest_user_ids ( rootfs : & Path ) -> Result < Option < ( u32 , u32 ) > , String > {
@@ -983,92 +962,82 @@ fn run_debugfs_batch(image_path: &Path, commands: &[String]) -> Result<(), Strin
983962}
984963
985964fn run_debugfs_batch_file ( image_path : & Path , command_path : & Path ) -> Result < ( ) , String > {
986- let mut last_error = None ;
987- for candidate in e2fs_tool_candidates ( "debugfs" ) {
988- let label = candidate. display ( ) . to_string ( ) ;
989- let output = Command :: new ( & candidate)
990- . arg ( "-w" )
991- . arg ( "-f" )
992- . arg ( command_path)
993- . arg ( image_path)
994- . output ( ) ;
995- match output {
996- Ok ( output) if output. status . success ( ) => return Ok ( ( ) ) ,
997- Ok ( output) => {
998- last_error = Some ( format ! (
999- "{label} failed with status {}\n stdout: {}\n stderr: {}" ,
1000- output. status,
1001- String :: from_utf8_lossy( & output. stdout) ,
1002- String :: from_utf8_lossy( & output. stderr)
1003- ) ) ;
1004- }
1005- Err ( err) if err. kind ( ) == std:: io:: ErrorKind :: NotFound => {
1006- last_error = Some ( format ! ( "{label} not found" ) ) ;
1007- }
1008- Err ( err) => {
1009- last_error = Some ( format ! ( "run {label}: {err}" ) ) ;
1010- }
1011- }
965+ let output = e2fs_command ( "debugfs" ) ?
966+ . arg ( "-w" )
967+ . arg ( "-f" )
968+ . arg ( command_path)
969+ . arg ( image_path)
970+ . output ( ) ;
971+ match output {
972+ Ok ( output) if output. status . success ( ) => Ok ( ( ) ) ,
973+ Ok ( output) => Err ( format ! (
974+ "debugfs batch {} failed for {}: debugfs failed with status {}\n stdout: {}\n stderr: {}. Install e2fsprogs (debugfs) and retry" ,
975+ command_path. display( ) ,
976+ image_path. display( ) ,
977+ output. status,
978+ String :: from_utf8_lossy( & output. stdout) ,
979+ String :: from_utf8_lossy( & output. stderr)
980+ ) ) ,
981+ Err ( error) => Err ( format ! (
982+ "debugfs batch {} failed for {}: {error}. Install e2fsprogs (debugfs) and retry" ,
983+ command_path. display( ) ,
984+ image_path. display( )
985+ ) ) ,
1012986 }
1013- Err ( format ! (
1014- "debugfs batch {} failed for {}: {}. Install e2fsprogs (debugfs) and retry" ,
1015- command_path. display( ) ,
1016- image_path. display( ) ,
1017- last_error. unwrap_or_else( || "debugfs not found" . to_string( ) )
1018- ) )
1019987}
1020988
1021989fn run_debugfs ( image_path : & Path , command : & str ) -> Result < ( ) , String > {
1022- let mut last_error = None ;
1023- for candidate in e2fs_tool_candidates ( "debugfs" ) {
1024- let label = candidate. display ( ) . to_string ( ) ;
1025- let output = Command :: new ( & candidate)
1026- . arg ( "-w" )
1027- . arg ( "-R" )
1028- . arg ( command)
1029- . arg ( image_path)
1030- . output ( ) ;
1031- match output {
1032- Ok ( output) if output. status . success ( ) => return Ok ( ( ) ) ,
1033- Ok ( output) => {
1034- last_error = Some ( format ! (
1035- "{label} failed with status {}\n stdout: {}\n stderr: {}" ,
1036- output. status,
1037- String :: from_utf8_lossy( & output. stdout) ,
1038- String :: from_utf8_lossy( & output. stderr)
1039- ) ) ;
1040- }
1041- Err ( err) if err. kind ( ) == std:: io:: ErrorKind :: NotFound => {
1042- last_error = Some ( format ! ( "{label} not found" ) ) ;
1043- }
1044- Err ( err) => {
1045- last_error = Some ( format ! ( "run {label}: {err}" ) ) ;
1046- }
1047- }
990+ let output = e2fs_command ( "debugfs" ) ?
991+ . arg ( "-w" )
992+ . arg ( "-R" )
993+ . arg ( command)
994+ . arg ( image_path)
995+ . output ( ) ;
996+ match output {
997+ Ok ( output) if output. status . success ( ) => Ok ( ( ) ) ,
998+ Ok ( output) => Err ( format ! (
999+ "debugfs command '{command}' failed for {}: debugfs failed with status {}\n stdout: {}\n stderr: {}. Install e2fsprogs (debugfs) and retry" ,
1000+ image_path. display( ) ,
1001+ output. status,
1002+ String :: from_utf8_lossy( & output. stdout) ,
1003+ String :: from_utf8_lossy( & output. stderr)
1004+ ) ) ,
1005+ Err ( error) => Err ( format ! (
1006+ "debugfs command '{command}' failed for {}: {error}. Install e2fsprogs (debugfs) and retry" ,
1007+ image_path. display( )
1008+ ) ) ,
10481009 }
1049- Err ( format ! (
1050- "debugfs command '{command}' failed for {}: {}. Install e2fsprogs (debugfs) and retry" ,
1051- image_path. display( ) ,
1052- last_error. unwrap_or_else( || "debugfs not found" . to_string( ) )
1053- ) )
10541010}
10551011
1056- /// Resolve the places an e2fsprogs binary may live, starting with `PATH`.
1057- ///
1058- /// Distributions install `mke2fs`, `e2fsck`, and `debugfs` under `/usr/sbin`
1059- /// or `/sbin`, which most Linux hosts leave out of a non-root user's `PATH`,
1060- /// so those directories are probed explicitly. Homebrew keeps e2fsprogs
1061- /// keg-only on macOS, so its prefixes are probed as well.
1062- fn e2fs_tool_candidates ( tool : & str ) -> Vec < PathBuf > {
1063- let mut candidates = vec ! [ PathBuf :: from( tool) ] ;
1064- for dir in [ "/usr/local/sbin" , "/usr/sbin" , "/sbin" ] {
1065- candidates. push ( Path :: new ( dir) . join ( tool) ) ;
1066- }
1067- for root in [ "/opt/homebrew/opt/e2fsprogs" , "/usr/local/opt/e2fsprogs" ] {
1068- candidates. push ( Path :: new ( root) . join ( "sbin" ) . join ( tool) ) ;
1069- candidates. push ( Path :: new ( root) . join ( "bin" ) . join ( tool) ) ;
1070- }
1071- candidates
1012+ fn e2fs_command ( tool : impl AsRef < OsStr > ) -> Result < Command , String > {
1013+ let path = e2fs_search_path ( std:: env:: var_os ( "PATH" ) . as_deref ( ) ) ?;
1014+ let mut command = Command :: new ( tool) ;
1015+ command. env ( "PATH" , path) ;
1016+ Ok ( command)
1017+ }
1018+
1019+ fn e2fs_search_path ( existing : Option < & OsStr > ) -> Result < OsString , String > {
1020+ let mut paths = existing
1021+ . map ( std:: env:: split_paths)
1022+ . into_iter ( )
1023+ . flatten ( )
1024+ . collect :: < Vec < _ > > ( ) ;
1025+
1026+ #[ cfg( target_os = "linux" ) ]
1027+ paths. extend ( [ "/usr/local/sbin" , "/usr/sbin" , "/sbin" ] . map ( PathBuf :: from) ) ;
1028+
1029+ #[ cfg( target_os = "macos" ) ]
1030+ paths. extend (
1031+ [
1032+ "/opt/homebrew/opt/e2fsprogs/sbin" ,
1033+ "/opt/homebrew/opt/e2fsprogs/bin" ,
1034+ "/usr/local/opt/e2fsprogs/sbin" ,
1035+ "/usr/local/opt/e2fsprogs/bin" ,
1036+ ]
1037+ . map ( PathBuf :: from) ,
1038+ ) ;
1039+
1040+ std:: env:: join_paths ( paths) . map_err ( |error| format ! ( "construct e2fsprogs PATH: {error}" ) )
10721041}
10731042
10741043fn temporary_injection_path ( image_path : & Path ) -> PathBuf {
@@ -1513,10 +1482,15 @@ mod tests {
15131482
15141483 #[ test]
15151484 fn recover_rootfs_image_accepts_clean_ext4_image ( ) {
1516- if !e2fs_tool_candidates ( "e2fsck" )
1517- . iter ( )
1518- . any ( |candidate| Command :: new ( candidate) . arg ( "-V" ) . output ( ) . is_ok ( ) )
1519- {
1485+ let available = e2fs_command ( "e2fsck" )
1486+ . and_then ( |mut command| {
1487+ command
1488+ . arg ( "-V" )
1489+ . output ( )
1490+ . map_err ( |error| error. to_string ( ) )
1491+ } )
1492+ . is_ok ( ) ;
1493+ if !available {
15201494 return ;
15211495 }
15221496
@@ -1595,17 +1569,31 @@ mod tests {
15951569 }
15961570
15971571 #[ test]
1598- fn e2fs_tool_candidates_probe_path_then_system_sbin_directories ( ) {
1599- let candidates = e2fs_tool_candidates ( "mke2fs" ) ;
1600-
1601- assert_eq ! ( candidates. first( ) , Some ( & PathBuf :: from( "mke2fs" ) ) ) ;
1602- for expected in [ "/usr/local/sbin/mke2fs" , "/usr/sbin/mke2fs" , "/sbin/mke2fs" ] {
1603- assert ! (
1604- candidates. contains( & PathBuf :: from( expected) ) ,
1605- "expected {expected} in {candidates:?}"
1606- ) ;
1607- }
1608- assert ! ( candidates. contains( & PathBuf :: from( "/opt/homebrew/opt/e2fsprogs/sbin/mke2fs" ) ) ) ;
1572+ fn e2fs_search_path_preserves_path_before_platform_fallbacks ( ) {
1573+ let search_path = e2fs_search_path ( Some ( OsStr :: new ( "/custom/bin" ) ) )
1574+ . expect ( "construct e2fsprogs search path" ) ;
1575+ let paths = std:: env:: split_paths ( & search_path) . collect :: < Vec < _ > > ( ) ;
1576+
1577+ assert_eq ! ( paths. first( ) , Some ( & PathBuf :: from( "/custom/bin" ) ) ) ;
1578+ #[ cfg( target_os = "linux" ) ]
1579+ assert_eq ! (
1580+ & paths[ 1 ..] ,
1581+ [
1582+ PathBuf :: from( "/usr/local/sbin" ) ,
1583+ PathBuf :: from( "/usr/sbin" ) ,
1584+ PathBuf :: from( "/sbin" ) ,
1585+ ]
1586+ ) ;
1587+ #[ cfg( target_os = "macos" ) ]
1588+ assert_eq ! (
1589+ & paths[ 1 ..] ,
1590+ [
1591+ PathBuf :: from( "/opt/homebrew/opt/e2fsprogs/sbin" ) ,
1592+ PathBuf :: from( "/opt/homebrew/opt/e2fsprogs/bin" ) ,
1593+ PathBuf :: from( "/usr/local/opt/e2fsprogs/sbin" ) ,
1594+ PathBuf :: from( "/usr/local/opt/e2fsprogs/bin" ) ,
1595+ ]
1596+ ) ;
16091597 }
16101598
16111599 #[ test]
0 commit comments