@@ -639,4 +639,243 @@ impl<'b> Squashfs<'b> {
639639 } ;
640640 Ok ( filesystem)
641641 }
642+
643+ /// Extract the Squashfs into `dest`
644+ #[ cfg( feature = "util" ) ]
645+ pub fn unsquashfs (
646+ self ,
647+ dest : & std:: path:: Path ,
648+ path_filter : Option < PathBuf > ,
649+ force : bool ,
650+ ) -> Result < ( ) , BackhandError > {
651+ use std:: fs:: { self , File } ;
652+ use std:: io:: { self } ;
653+ use std:: os:: unix:: fs:: lchown;
654+ use std:: path:: { Component , Path } ;
655+
656+ use nix:: {
657+ libc:: geteuid,
658+ sys:: stat:: { dev_t, mknod, mode_t, umask, utimensat, Mode , SFlag , UtimensatFlags } ,
659+ sys:: time:: TimeSpec ,
660+ } ;
661+ use rayon:: prelude:: * ;
662+
663+ // Quick hack to ensure we reset `umask` even when we return due to an error
664+ struct UmaskGuard {
665+ old : Mode ,
666+ }
667+ impl UmaskGuard {
668+ fn new ( mode : Mode ) -> Self {
669+ let old = umask ( mode) ;
670+ Self { old }
671+ }
672+ }
673+ impl Drop for UmaskGuard {
674+ fn drop ( & mut self ) {
675+ umask ( self . old ) ;
676+ }
677+ }
678+
679+ let root_process = unsafe { geteuid ( ) == 0 } ;
680+
681+ // FIXME: Do we want to set `umask` here, or leave it up to the caller?
682+ let _umask_guard = root_process. then ( || UmaskGuard :: new ( Mode :: from_bits ( 0 ) . unwrap ( ) ) ) ;
683+
684+ let filesystem = self . into_filesystem_reader ( ) ?;
685+
686+ let path_filter = path_filter. unwrap_or ( PathBuf :: from ( "/" ) ) ;
687+
688+ // if we can find a parent, then a filter must be applied and the exact parent dirs must be
689+ // found above it
690+ let mut files: Vec < & Node < SquashfsFileReader > > = vec ! [ ] ;
691+ if path_filter. parent ( ) . is_some ( ) {
692+ let mut current = PathBuf :: new ( ) ;
693+ current. push ( "/" ) ;
694+ for part in path_filter. iter ( ) {
695+ current. push ( part) ;
696+ if let Some ( exact) = filesystem. files ( ) . find ( |& a| a. fullpath == current) {
697+ files. push ( exact) ;
698+ } else {
699+ return Err ( BackhandError :: InvalidPathFilter ( path_filter) ) ;
700+ }
701+ }
702+ // remove the final node, this is a file and will be caught in the following statement
703+ files. pop ( ) ;
704+ }
705+
706+ // gather all files and dirs
707+ let nodes = files
708+ . into_iter ( )
709+ . chain ( filesystem. files ( ) . filter ( |a| a. fullpath . starts_with ( & path_filter) ) )
710+ . collect :: < Vec < _ > > ( ) ;
711+
712+ nodes
713+ . into_par_iter ( )
714+ . map ( |node| {
715+ let path = & node. fullpath ;
716+ let fullpath = path. strip_prefix ( Component :: RootDir ) . unwrap_or ( path) ;
717+
718+ let filepath = Path :: new ( & dest) . join ( fullpath) ;
719+ // create required dirs, we will fix permissions later
720+ let _ = fs:: create_dir_all ( filepath. parent ( ) . unwrap ( ) ) ;
721+
722+ match & node. inner {
723+ InnerNode :: File ( file) => {
724+ // alloc required space for file data readers
725+ let ( mut buf_read, mut buf_decompress) = filesystem. alloc_read_buffers ( ) ;
726+
727+ // check if file exists
728+ if !force && filepath. exists ( ) {
729+ trace ! ( path=%filepath. display( ) , "file exists" ) ;
730+ return Ok ( ( ) ) ;
731+ }
732+
733+ // write to file
734+ let mut fd = File :: create ( & filepath) ?;
735+ let file = filesystem. file ( & file. basic ) ;
736+ let mut reader = file. reader ( & mut buf_read, & mut buf_decompress) ;
737+
738+ io:: copy ( & mut reader, & mut fd) . map_err ( |e| {
739+ BackhandError :: UnsquashFile { source : e, path : filepath. clone ( ) }
740+ } ) ?;
741+ trace ! ( path=%filepath. display( ) , "unsquashed file" ) ;
742+ }
743+ InnerNode :: Symlink ( SquashfsSymlink { link } ) => {
744+ // check if file exists
745+ if !force && filepath. exists ( ) {
746+ trace ! ( path=%filepath. display( ) , "symlink exists" ) ;
747+ return Ok ( ( ) ) ;
748+ }
749+ // create symlink
750+ std:: os:: unix:: fs:: symlink ( link, & filepath) . map_err ( |e| {
751+ BackhandError :: UnsquashSymlink {
752+ source : e,
753+ from : link. to_path_buf ( ) ,
754+ to : filepath. clone ( ) ,
755+ }
756+ } ) ?;
757+ // set attributes, but special to not follow the symlink
758+ // TODO: unify with set_attributes?
759+ if root_process {
760+ // TODO: Use (unix_chown) when not nightly: https://github.com/rust-lang/rust/issues/88989
761+ lchown ( & filepath, Some ( node. header . uid ) , Some ( node. header . gid ) )
762+ . map_err ( |e| BackhandError :: SetAttributes {
763+ source : e,
764+ path : filepath. to_path_buf ( ) ,
765+ } ) ?;
766+ }
767+
768+ // TODO Use (file_set_times) when not nightly: https://github.com/rust-lang/rust/issues/98245
769+ // Make sure this doesn't follow symlinks when changed to std library!
770+ let timespec = TimeSpec :: new ( node. header . mtime as _ , 0 ) ;
771+ utimensat (
772+ None ,
773+ & filepath,
774+ & timespec,
775+ & timespec,
776+ UtimensatFlags :: NoFollowSymlink ,
777+ )
778+ . map_err ( |e| BackhandError :: SetUtimes {
779+ source : e,
780+ path : filepath. clone ( ) ,
781+ } ) ?;
782+ trace ! ( from=%link. display( ) , to=%filepath. display( ) , "unsquashed symlink" ) ;
783+ }
784+ InnerNode :: Dir ( SquashfsDir { .. } ) => {
785+ // These permissions are corrected later (user default permissions for now)
786+ //
787+ // don't display error if this was already created, we might have already
788+ // created it in another thread to put down a file
789+ if std:: fs:: create_dir ( & filepath) . is_ok ( ) {
790+ trace ! ( path=%filepath. display( ) , "unsquashed dir" ) ;
791+ }
792+ }
793+ InnerNode :: CharacterDevice ( SquashfsCharacterDevice { device_number } ) => {
794+ mknod (
795+ & filepath,
796+ SFlag :: S_IFCHR ,
797+ Mode :: from_bits ( mode_t:: from ( node. header . permissions ) ) . unwrap ( ) ,
798+ dev_t:: try_from ( * device_number) . unwrap ( ) ,
799+ )
800+ . map_err ( |e| BackhandError :: UnsquashCharDev {
801+ source : e,
802+ path : filepath. clone ( ) ,
803+ } ) ?;
804+ set_attributes ( & filepath, & node. header , root_process, true ) ?;
805+ trace ! ( path=%filepath. display( ) , "unsquashed character device" ) ;
806+ }
807+ InnerNode :: BlockDevice ( SquashfsBlockDevice { device_number } ) => {
808+ mknod (
809+ & filepath,
810+ SFlag :: S_IFBLK ,
811+ Mode :: from_bits ( mode_t:: from ( node. header . permissions ) ) . unwrap ( ) ,
812+ dev_t:: try_from ( * device_number) . unwrap ( ) ,
813+ )
814+ . map_err ( |e| BackhandError :: UnsquashBlockDev {
815+ source : e,
816+ path : filepath. clone ( ) ,
817+ } ) ?;
818+ set_attributes ( & filepath, & node. header , root_process, true ) ?;
819+ trace ! ( path=%filepath. display( ) , "unsquashed block device" ) ;
820+ }
821+ }
822+ Ok ( ( ) )
823+ } )
824+ . collect :: < Result < ( ) , BackhandError > > ( ) ?;
825+
826+ // fixup dir permissions
827+ for node in filesystem. files ( ) . filter ( |a| a. fullpath . starts_with ( & path_filter) ) {
828+ if let InnerNode :: Dir ( SquashfsDir { .. } ) = & node. inner {
829+ let path = & node. fullpath ;
830+ let path = path. strip_prefix ( Component :: RootDir ) . unwrap_or ( path) ;
831+ let path = Path :: new ( & dest) . join ( path) ;
832+ set_attributes ( & path, & node. header , root_process, false ) ?;
833+ }
834+ }
835+
836+ Ok ( ( ) )
837+ }
838+ }
839+
840+ #[ cfg( feature = "util" ) ]
841+ fn set_attributes (
842+ path : & std:: path:: Path ,
843+ header : & NodeHeader ,
844+ root_process : bool ,
845+ is_file : bool ,
846+ ) -> Result < ( ) , BackhandError > {
847+ // TODO Use (file_set_times) when not nightly: https://github.com/rust-lang/rust/issues/98245
848+ use nix:: { sys:: stat:: utimes, sys:: time:: TimeVal } ;
849+ use std:: os:: unix:: fs:: lchown;
850+
851+ use std:: { fs:: Permissions , os:: unix:: fs:: PermissionsExt } ;
852+ let timeval = TimeVal :: new ( header. mtime as _ , 0 ) ;
853+ utimes ( path, & timeval, & timeval)
854+ . map_err ( |e| BackhandError :: SetUtimes { source : e, path : path. to_path_buf ( ) } ) ?;
855+
856+ let mut mode = u32:: from ( header. permissions ) ;
857+
858+ // Only chown when root
859+ if root_process {
860+ // TODO: Use (unix_chown) when not nightly: https://github.com/rust-lang/rust/issues/88989
861+ lchown ( path, Some ( header. uid ) , Some ( header. gid ) )
862+ . map_err ( |e| BackhandError :: SetAttributes { source : e, path : path. to_path_buf ( ) } ) ?;
863+ } else if is_file {
864+ // bitwise-not if not rooted (disable write permissions for user/group). Following
865+ // squashfs-tools/unsquashfs behavior
866+ mode &= !0o022 ;
867+ }
868+
869+ // set permissions
870+ //
871+ // NOTE: In squashfs-tools/unsquashfs they remove the write bits for user and group?
872+ // I don't know if there is a reason for that but I keep the permissions the same if possible
873+ match std:: fs:: set_permissions ( path, Permissions :: from_mode ( mode) ) {
874+ Ok ( _) => return Ok ( ( ) ) ,
875+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: PermissionDenied => { }
876+ Err ( e) => return Err ( BackhandError :: SetAttributes { source : e, path : path. to_path_buf ( ) } ) ,
877+ } ;
878+ // retry without sticky bit
879+ std:: fs:: set_permissions ( path, Permissions :: from_mode ( mode & !1000 ) )
880+ . map_err ( |e| BackhandError :: SetAttributes { source : e, path : path. to_path_buf ( ) } )
642881}
0 commit comments