@@ -276,6 +276,11 @@ func SCPDirFromContextE(t testing.TestingT, ctx context.Context, options *SCPDow
276276 logger .Default .Logf (t , "Copying remote file: %s to local path %s" , fullRemoteFilePath , localFilePath )
277277
278278 err = copyFileFromRemote (ctx , t , sshSession , localFile , fullRemoteFilePath , useSudo )
279+ // Close the local file regardless of copy outcome so we do not leak file handles.
280+ if closeErr := localFile .Close (); closeErr != nil && err == nil {
281+ err = closeErr
282+ }
283+
279284 errorsOccurred = multierror .Append (errorsOccurred , err )
280285 }
281286
@@ -651,11 +656,18 @@ func FetchContentsOfFileContext(t testing.TestingT, ctx context.Context, host *H
651656 return out
652657}
653658
659+ // shellQuote wraps a path in single quotes, escaping any embedded single quotes,
660+ // so that paths containing spaces or shell metacharacters work correctly when
661+ // passed to commands like `cat` and `dd if=`.
662+ func shellQuote (s string ) string {
663+ return "'" + strings .ReplaceAll (s , "'" , `'\''` ) + "'"
664+ }
665+
654666// FetchContentsOfFileContextE connects to the given host via SSH and fetches the contents of the file at the given filePath.
655667// If useSudo is true, then the contents will be retrieved using sudo. Returns the contents of that file.
656668// The ctx parameter supports cancellation and timeouts.
657669func FetchContentsOfFileContextE (t testing.TestingT , ctx context.Context , host * Host , useSudo bool , filePath string ) (string , error ) {
658- command := "cat " + filePath
670+ command := "cat " + shellQuote ( filePath )
659671 if useSudo {
660672 command = "sudo " + command
661673 }
@@ -707,15 +719,22 @@ func listFileInRemoteDir(ctx context.Context, t testing.TestingT, sshSession *SS
707719
708720 // The last character returned is `\n` this results in an extra "" array
709721 // member when we do the split below. Cut off the last character to avoid
710- // having to remove the blank entry in the array.
711- resultString = resultString [:len (resultString )- 1 ]
722+ // having to remove the blank entry in the array. Guard against empty output
723+ // so we do not panic with index out of range.
724+ if len (resultString ) > 0 {
725+ resultString = resultString [:len (resultString )- 1 ]
726+ }
712727
713728 return strings .Split (resultString , "\n " ), nil
714729}
715730
716731// copyFileFromRemote copies a file from a remote host to a local file.
717732// Based on code: https://github.com/bramvdbogaerde/go-scp/pull/6/files
718733func copyFileFromRemote (ctx context.Context , t testing.TestingT , sshSession * SSHSession , file * os.File , remotePath string , useSudo bool ) error {
734+ // Ensure the local file handle is always closed; the caller passes us an
735+ // open *os.File and we own its lifetime from here.
736+ defer func () { _ = file .Close () }()
737+
719738 if err := setUpSSHClient (ctx , sshSession ); err != nil {
720739 return err
721740 }
@@ -724,20 +743,20 @@ func copyFileFromRemote(ctx context.Context, t testing.TestingT, sshSession *SSH
724743 return err
725744 }
726745
727- command := "dd if=" + remotePath
746+ command := "dd if=" + shellQuote ( remotePath )
728747 if useSudo {
729748 command = "sudo " + command
730749 }
731750
732751 logger .Default .Logf (t , "Running command %s on %s@%s" , command , sshSession .Options .Username , sshSession .Options .Address )
733752
753+ defer func () { _ = sshSession .Session .Close () }()
754+
734755 r , err := sshSession .Session .Output (command )
735756 if err != nil {
736- logger . Default . Logf ( t , "error reading from remote stdout: %s " , err )
757+ return fmt . Errorf ( "error reading from remote stdout: %w " , err )
737758 }
738759
739- defer func () { _ = sshSession .Session .Close () }()
740-
741760 // Write to local file.
742761 _ , err = file .Write (r )
743762
0 commit comments