11use std:: {
22 env, fs,
3- io:: Write ,
3+ io:: { Read , Write } ,
4+ os:: unix:: fs:: PermissionsExt ,
45 path:: { Path , PathBuf } ,
56 process:: Command ,
67 thread:: sleep,
@@ -35,10 +36,23 @@ use crate::{
3536 constants:: INSTALL_MARKER_FILE ,
3637 database:: { connection:: DieselDatabase , models:: Package } ,
3738 error:: { ErrorContext , SoarError } ,
39+ package:: local:: local_path_from_url,
3840 utils:: get_extract_dir,
3941 SoarResult ,
4042} ;
4143
44+ /// Returns `true` if the file at `path` starts with the ELF magic bytes.
45+ ///
46+ /// AppImages and plain binaries are ELF and need the executable bit; archives
47+ /// are not and are extracted instead.
48+ fn is_elf ( path : & Path ) -> bool {
49+ let mut magic = [ 0u8 ; 4 ] ;
50+ fs:: File :: open ( path)
51+ . and_then ( |mut f| f. read_exact ( & mut magic) )
52+ . is_ok ( )
53+ && magic == * b"\x7f ELF"
54+ }
55+
4256/// Early validation of relative paths before download.
4357/// Rejects paths containing `..` or absolute paths.
4458fn validate_relative_path ( relative_path : & str , path_type : & str ) -> SoarResult < ( ) > {
@@ -527,6 +541,67 @@ impl PackageInstaller {
527541 Ok ( ( ) )
528542 }
529543
544+ /// Install a package from a local file by copying it into the install
545+ /// directory (and extracting it when it is an archive), mirroring the
546+ /// relevant post-download steps of [`Download::execute`].
547+ fn copy_local_source (
548+ & self ,
549+ src : & Path ,
550+ dest : & Path ,
551+ extract : bool ,
552+ extract_dir : & Path ,
553+ ) -> SoarResult < PathBuf > {
554+ if !src. is_file ( ) {
555+ return Err ( SoarError :: Custom ( format ! (
556+ "Local source is not a file: {}" ,
557+ src. display( )
558+ ) ) ) ;
559+ }
560+
561+ if let Some ( parent) = dest. parent ( ) {
562+ fs:: create_dir_all ( parent)
563+ . with_context ( || format ! ( "creating directory {}" , parent. display( ) ) ) ?;
564+ }
565+
566+ fs:: copy ( src, dest) . with_context ( || {
567+ format ! ( "copying {} to {}" , src. display( ) , dest. display( ) )
568+ } ) ?;
569+
570+ // Honor checksum pinning the same way the direct-download path does.
571+ if let Some ( ref bsum) = self . package . bsum {
572+ let actual = calculate_checksum ( dest) ?;
573+ if & actual != bsum {
574+ fs:: remove_file ( dest) . ok ( ) ;
575+ return Err ( SoarError :: Custom ( format ! (
576+ "Checksum mismatch for {}: expected {}, got {}" ,
577+ src. display( ) ,
578+ bsum,
579+ actual
580+ ) ) ) ;
581+ }
582+ }
583+
584+ // ELF binaries (including AppImages) need the executable bit; archives
585+ // are extracted below instead of being run directly.
586+ if is_elf ( dest) {
587+ fs:: set_permissions ( dest, std:: fs:: Permissions :: from_mode ( 0o755 ) )
588+ . with_context ( || format ! ( "setting permissions on {}" , dest. display( ) ) ) ?;
589+ }
590+
591+ if extract {
592+ debug ! ( archive = %dest. display( ) , dest = %extract_dir. display( ) , "extracting local archive" ) ;
593+ compak:: extract_archive ( dest, extract_dir) . map_err ( |e| {
594+ SoarError :: Custom ( format ! (
595+ "Failed to extract archive {}: {}" ,
596+ dest. display( ) ,
597+ e
598+ ) )
599+ } ) ?;
600+ }
601+
602+ Ok ( dest. to_path_buf ( ) )
603+ }
604+
530605 pub async fn download_package ( & self ) -> SoarResult < Option < String > > {
531606 debug ! (
532607 pkg_name = self . package. pkg_name,
@@ -621,7 +696,6 @@ impl PackageInstaller {
621696
622697 Ok ( None )
623698 } else {
624- trace ! ( url = url. as_str( ) , "using direct download" ) ;
625699 let extract_dir = get_extract_dir ( & self . install_dir ) ;
626700
627701 let should_extract = self
@@ -630,24 +704,30 @@ impl PackageInstaller {
630704 . as_deref ( )
631705 . is_some_and ( |t| t == "archive" ) ;
632706
633- let mut dl = Download :: new ( url. as_str ( ) )
634- . output ( output_path. to_string_lossy ( ) )
635- . overwrite ( OverwriteMode :: Skip )
636- . extract ( should_extract)
637- . extract_to ( & extract_dir) ;
638-
639- if let Some ( ref bsum) = self . package . bsum {
640- dl = dl. checksum ( bsum) ;
641- }
707+ let file_path = if let Some ( local_src) = local_path_from_url ( url) {
708+ trace ! ( source = %local_src. display( ) , "installing from local file" ) ;
709+ self . copy_local_source ( local_src, output_path, should_extract, & extract_dir) ?
710+ } else {
711+ trace ! ( url = url. as_str( ) , "using direct download" ) ;
712+ let mut dl = Download :: new ( url. as_str ( ) )
713+ . output ( output_path. to_string_lossy ( ) )
714+ . overwrite ( OverwriteMode :: Skip )
715+ . extract ( should_extract)
716+ . extract_to ( & extract_dir) ;
717+
718+ if let Some ( ref bsum) = self . package . bsum {
719+ dl = dl. checksum ( bsum) ;
720+ }
642721
643- if let Some ( ref cb) = self . progress_callback {
644- let cb = cb. clone ( ) ;
645- dl = dl. progress ( move |p| {
646- cb ( p) ;
647- } ) ;
648- }
722+ if let Some ( ref cb) = self . progress_callback {
723+ let cb = cb. clone ( ) ;
724+ dl = dl. progress ( move |p| {
725+ cb ( p) ;
726+ } ) ;
727+ }
649728
650- let file_path = dl. execute ( ) ?;
729+ dl. execute ( ) ?
730+ } ;
651731
652732 self . run_post_download_hook ( ) ?;
653733
0 commit comments