@@ -8,6 +8,10 @@ import (
88 "os"
99 "os/exec"
1010 "path/filepath"
11+ "runtime"
12+ "strconv"
13+ "strings"
14+ "syscall"
1115 "text/tabwriter"
1216)
1317
@@ -24,6 +28,12 @@ type Args struct {
2428 MD5 bool `arg:"--md5" help:"calculate MD5 checksums for files"`
2529 Keep bool `arg:"--keep" help:"keep the temporary container after inspection"`
2630 NoTimes bool `arg:"--no-times" help:"exclude modification times from output"`
31+ // for extraction
32+ OutputDir string `arg:"--output-dir" help:"extract matching files to this directory"`
33+ StripComponents int `arg:"--strip-components" help:"strip NUMBER leading components from file names"`
34+ PreserveOwner bool `arg:"--preserve-owner" help:"preserve user/group information when extracting"`
35+ PreservePermissions bool `arg:"--preserve-perms" help:"preserve file permissions when extracting"`
36+ PreserveAll bool `arg:"--preserve-all" help:"preserve all file attributes"`
2737}
2838
2939func (Args ) Version () string {
@@ -90,6 +100,36 @@ func runInspector(image string, args Args) ([]byte, error) {
90100 dockerArgs = append (dockerArgs , "--rm" )
91101 }
92102
103+ // If output directory is specified, mount it
104+ if args .OutputDir != "" {
105+ // Convert to absolute path
106+ absPath , err := filepath .Abs (args .OutputDir )
107+ if err != nil {
108+ return nil , fmt .Errorf ("failed to get absolute path for output dir: %v" , err )
109+ }
110+
111+ // Create the output directory if it doesn't exist
112+ if err := os .Mkdir (absPath , 0755 ); err != nil && ! os .IsExist (err ) {
113+ return nil , fmt .Errorf ("failed to create output directory: %v" , err )
114+ }
115+
116+ dockerArgs = append (dockerArgs ,
117+ "-v" , fmt .Sprintf ("%s:/inspect-target" , absPath ))
118+ }
119+
120+ /*
121+ // Add capabilities if we need to preserve ownership
122+ if args.OutputDir != "" && args.PreserveOwner {
123+ // Option 1: Full privileged mode (more than we need, but guaranteed to work)
124+ //dockerArgs = append(dockerArgs, "--privileged")
125+ // Option 2: Just the capabilities we need (more secure)
126+ dockerArgs = append(dockerArgs,
127+ "--cap-add=CHOWN",
128+ "--cap-add=DAC_OVERRIDE",
129+ "--cap-add=DAC_READ_SEARCH")
130+ }
131+ */
132+
93133 // Mount the inspector and set it as entrypoint
94134 dockerArgs = append (dockerArgs ,
95135 "-v" , fmt .Sprintf ("%s:/inspect:ro" , inspectorPath ),
@@ -109,13 +149,22 @@ func runInspector(image string, args Args) ([]byte, error) {
109149 if args .Path != "/" {
110150 dockerArgs = append (dockerArgs , "--path" , args .Path )
111151 }
112-
152+ if args .OutputDir != "" {
153+ dockerArgs = append (dockerArgs , "--output-dir" , "/inspect-target" )
154+ dockerArgs = append (dockerArgs , "--strip-components" , fmt .Sprintf ("%d" , args .StripComponents ))
155+ if args .PreserveOwner {
156+ dockerArgs = append (dockerArgs , "--preserve-owner" )
157+ }
158+ if args .PreservePermissions {
159+ dockerArgs = append (dockerArgs , "--preserve-perms" )
160+ }
161+ }
113162 // Create a pipe for capturing stdout while also displaying it
114163 cmd := exec .Command ("docker" , dockerArgs ... )
115164 cmd .Stderr = os .Stderr
165+ cmd .Stderr = os .Stderr
116166 output , err := cmd .Output ()
117167 return output , err
118-
119168 /*
120169 // This is a version that lets us debug what the docker command is printing
121170 stdout, err := cmd.StdoutPipe()
@@ -161,6 +210,18 @@ func main() {
161210
162211 arg .MustParse (& args )
163212
213+ if args .PreserveAll {
214+ args .PreserveOwner = true
215+ args .PreservePermissions = true
216+ }
217+ // check if we actually can handle the owner preservation
218+ if runtime .GOOS == "darwin" && args .OutputDir != "" && args .PreserveOwner {
219+ if ! isOwnershipSupported (args .OutputDir ) {
220+ fmt .Fprintf (os .Stderr , "filesystem of %q does not support ownership changes\n " , args .OutputDir )
221+ os .Exit (1 )
222+ }
223+ }
224+
164225 // Run inspection on first image
165226 files1JSON , err := runInspector (args .Image1 , args )
166227 if err != nil {
@@ -214,11 +275,11 @@ func main() {
214275 os .Exit (1 )
215276 }
216277 } else {
278+ var files1 []FileInfo
217279 if args .JSON {
218280 // we just print what we got
219281 fmt .Print (string (files1JSON ))
220282 } else {
221- var files1 []FileInfo
222283 if err := json .Unmarshal (files1JSON , & files1 ); err != nil {
223284 fmt .Fprintf (os .Stderr , "failed to parse inspection results: %v" , err )
224285 os .Exit (1 )
@@ -277,5 +338,157 @@ func main() {
277338 fmt .Printf ("Files: %d\n " , fileCount )
278339 }
279340 }
341+
342+ // If we're on macOS and files were copied with ownership preservation requested,
343+ // fix ownership using sudo
344+ if runtime .GOOS == "darwin" && args .OutputDir != "" &&
345+ args .PreserveOwner {
346+ // Test if ownership changes are supported
347+ if args .JSON {
348+ if err := json .Unmarshal (files1JSON , & files1 ); err != nil {
349+ fmt .Fprintf (os .Stderr , "failed to parse inspection results: %v" , err )
350+ os .Exit (1 )
351+ }
352+ }
353+ fmt .Fprintf (os .Stderr , "\n Fixing file ownership on macOS..." )
354+ if err := fixOwnershipWithSudo (files1 , args .OutputDir , args .StripComponents ); err != nil {
355+ fmt .Fprintf (os .Stderr , "\n Error fixing ownership: %v\n " , err )
356+ os .Exit (1 )
357+ }
358+ fmt .Fprintf (os .Stderr , " Done!\n " )
359+ }
360+ }
361+ }
362+
363+ // In main.go, modify the ownership fixing:
364+ func fixOwnershipWithSudo (files []FileInfo , outputDir string , stripComponents int ) error {
365+ // Build a script of chown commands
366+ var commands strings.Builder
367+ commands .WriteString ("#!/bin/bash\n " )
368+
369+ for _ , file := range files {
370+ // Get the adjusted path based on strip components
371+ destPath := getDestPath (file .Path , stripComponents )
372+ if destPath == "" {
373+ continue
374+ }
375+
376+ // Extract UID/GID from the user/group strings
377+ uid , err := extractID (file .User )
378+ if err != nil {
379+ fmt .Fprintf (os .Stderr , "Warning: Could not extract UID from %q: %v\n " , file .User , err )
380+ continue
381+ }
382+ gid , err := extractID (file .Group )
383+ if err != nil {
384+ fmt .Fprintf (os .Stderr , "Warning: Could not extract GID from %q: %v\n " , file .Group , err )
385+ continue
386+ }
387+
388+ fullDestPath := filepath .Join (outputDir , destPath )
389+ // Use -h to handle symlinks correctly
390+ fmt .Fprintf (& commands , "chown -h %d:%d %q\n " , uid , gid , fullDestPath )
391+ }
392+
393+ // Create a temporary script file
394+ scriptFile , err := os .CreateTemp ("" , "docker-inspector-*.sh" )
395+ if err != nil {
396+ return fmt .Errorf ("failed to create script file: %v" , err )
397+ }
398+ defer os .Remove (scriptFile .Name ())
399+
400+ if err := os .WriteFile (scriptFile .Name (), []byte (commands .String ()), 0700 ); err != nil {
401+ return fmt .Errorf ("failed to write script: %v" , err )
402+ }
403+
404+ //fmt.Println(commands.String())
405+ // Run the script with sudo
406+ cmd := exec .Command ("sudo" , "/bin/bash" , scriptFile .Name ())
407+ cmd .Stdout = os .Stdout
408+ cmd .Stderr = os .Stderr
409+
410+ if err := cmd .Run (); err != nil {
411+ return fmt .Errorf ("failed to fix ownership: %v" , err )
412+ }
413+
414+ return nil
415+ }
416+
417+ func getDestPath (sourcePath string , stripComponents int ) string {
418+ // Split path into components
419+ parts := strings .Split (strings .TrimPrefix (sourcePath , "/" ), "/" )
420+
421+ // Strip leading components
422+ if stripComponents >= len (parts ) {
423+ return ""
424+ }
425+
426+ return "/" + filepath .Join (parts [stripComponents :]... )
427+ }
428+
429+ func extractID (s string ) (int , error ) {
430+ // Find the last pair of parentheses
431+ openIdx := strings .LastIndex (s , "(" )
432+ closeIdx := strings .LastIndex (s , ")" )
433+ if openIdx == - 1 || closeIdx == - 1 || openIdx >= closeIdx {
434+ return 0 , fmt .Errorf ("no ID found in %q" , s )
435+ }
436+
437+ // Extract and parse the ID
438+ idStr := s [openIdx + 1 : closeIdx ]
439+ id , err := strconv .Atoi (idStr )
440+ if err != nil {
441+ return 0 , fmt .Errorf ("invalid ID in %q: %v" , s , err )
442+ }
443+ return id , nil
444+ }
445+
446+ func isOwnershipSupported (dir string ) bool {
447+ // Convert to absolute path
448+ absPath , err := filepath .Abs (dir )
449+ if err != nil {
450+ return false
451+ }
452+
453+ created := false
454+ stat , err := os .Stat (absPath )
455+ if err != nil {
456+ // Create the output directory if it doesn't exist
457+ if err := os .Mkdir (absPath , 0755 ); err != nil {
458+ return false
459+ }
460+ created = true
461+ }
462+ defer func () {
463+ if created {
464+ if err := os .Remove (absPath ); err != nil {
465+ fmt .Fprintf (os .Stderr , "failed to remove %q: %v" , absPath , err )
466+ }
467+ }
468+ }()
469+
470+ testFile , err := os .CreateTemp (dir , ".ownership-test-*" )
471+ if err != nil {
472+ return false
473+ }
474+ testPath := testFile .Name ()
475+ testFile .Close ()
476+ defer os .Remove (testPath )
477+
478+ fmt .Fprintf (os .Stderr , "Checking filesystem of %q for ownership support (requires sudo)...\n " , dir )
479+ // Try to change ownership to root:root
480+ if err := exec .Command ("sudo" , "chown" , "999:999" , testPath ).Run (); err != nil {
481+ return false
482+ }
483+
484+ // Read back the ownership
485+ stat , err = os .Stat (testPath )
486+ if err != nil {
487+ return false
488+ }
489+
490+ if sys , ok := stat .Sys ().(* syscall.Stat_t ); ok {
491+ return sys .Uid == 999 && sys .Gid == 999
280492 }
493+ return false
281494}
0 commit comments