@@ -8,10 +8,12 @@ import (
88 "errors"
99 "fmt"
1010 "io"
11+ "io/fs"
1112 "maps"
1213 "net/http"
1314 "os"
1415 "path/filepath"
16+ "strings"
1517
1618 "github.com/containers/common/libimage"
1719 "github.com/containers/image/v5/manifest"
@@ -254,6 +256,166 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, paths []string, op
254256 return & artifactManifestDigest , nil
255257}
256258
259+ // Inspect an artifact in a local store
260+ func (as ArtifactStore ) Extract (ctx context.Context , nameOrDigest string , target string , options * libartTypes.ExtractOptions ) error {
261+ if len (options .Digest ) > 0 && len (options .Title ) > 0 {
262+ return errors .New ("cannot specify both digest and title" )
263+ }
264+ if len (nameOrDigest ) == 0 {
265+ return ErrEmptyArtifactName
266+ }
267+
268+ artifacts , err := as .getArtifacts (ctx , nil )
269+ if err != nil {
270+ return err
271+ }
272+
273+ arty , nameIsDigest , err := artifacts .GetByNameOrDigest (nameOrDigest )
274+ if err != nil {
275+ return err
276+ }
277+ name := nameOrDigest
278+ if nameIsDigest {
279+ name = arty .Name
280+ }
281+
282+ if len (arty .Manifest .Layers ) == 0 {
283+ return fmt .Errorf ("the artifact has no blobs, nothing to extract" )
284+ }
285+
286+ ir , err := layout .NewReference (as .storePath , name )
287+ if err != nil {
288+ return err
289+ }
290+ imgSrc , err := ir .NewImageSource (ctx , as .SystemContext )
291+ if err != nil {
292+ return err
293+ }
294+ defer imgSrc .Close ()
295+
296+ // check if dest is a dir to know if we can copy more than one blob
297+ destIsFile := true
298+ stat , err := os .Stat (target )
299+ if err == nil {
300+ destIsFile = ! stat .IsDir ()
301+ } else if ! errors .Is (err , fs .ErrNotExist ) {
302+ return err
303+ }
304+
305+ if destIsFile {
306+ var digest digest.Digest
307+ if len (arty .Manifest .Layers ) > 1 {
308+ if len (options .Digest ) == 0 && len (options .Title ) == 0 {
309+ return fmt .Errorf ("the artifact consists of several blobs and the target %q is not a directory and neither digest or title was specified to only copy a single blob" , target )
310+ }
311+ digest , err = findDigest (arty , options )
312+ if err != nil {
313+ return err
314+ }
315+ } else {
316+ digest = arty .Manifest .Layers [0 ].Digest
317+ }
318+
319+ return copyImageBlobToFile (ctx , imgSrc , digest , target )
320+ }
321+
322+ if len (options .Digest ) > 0 || len (options .Title ) > 0 {
323+ digest , err := findDigest (arty , options )
324+ if err != nil {
325+ return err
326+ }
327+ // In case the digest is set we always use it as target name
328+ // so we do not have to get the actual title annotation form the blob.
329+ // Passing options.Title is enough because we know it is empty when digest
330+ // is set as we only allow either one.
331+ filename , err := generateArtifactBlobName (options .Title , digest )
332+ if err != nil {
333+ return err
334+ }
335+ return copyImageBlobToFile (ctx , imgSrc , digest , filepath .Join (target , filename ))
336+ }
337+
338+ for _ , l := range arty .Manifest .Layers {
339+ title := l .Annotations [specV1 .AnnotationTitle ]
340+ filename , err := generateArtifactBlobName (title , l .Digest )
341+ if err != nil {
342+ return err
343+ }
344+ err = copyImageBlobToFile (ctx , imgSrc , l .Digest , filepath .Join (target , filename ))
345+ if err != nil {
346+ return err
347+ }
348+ }
349+
350+ return nil
351+ }
352+
353+ func generateArtifactBlobName (title string , digest digest.Digest ) (string , error ) {
354+ filename := title
355+ if len (filename ) == 0 {
356+ // No filename given, use the digest. But because ":" is not a valid path char
357+ // on all platforms replace it with "-".
358+ filename = strings .ReplaceAll (digest .String (), ":" , "-" )
359+ }
360+
361+ // Important: A potentially malicious artifact could contain a title name with "/"
362+ // and could try via relative paths such as "../" try to overwrite files on the host
363+ // the user did not intend. As there is no use for directories in this path we
364+ // disallow all of them and not try to "make it safe" via securejoin or others.
365+ // We must use os.IsPathSeparator() as on Windows it checks both "\\" and "/".
366+ for i := 0 ; i < len (filename ); i ++ {
367+ if os .IsPathSeparator (filename [i ]) {
368+ return "" , fmt .Errorf ("invalid name: %q cannot contain %c" , filename , filename [i ])
369+ }
370+ }
371+ return filename , nil
372+ }
373+
374+ func findDigest (arty * libartifact.Artifact , options * libartTypes.ExtractOptions ) (digest.Digest , error ) {
375+ var digest digest.Digest
376+ for _ , l := range arty .Manifest .Layers {
377+ if options .Digest == l .Digest .String () {
378+ if len (digest .String ()) > 0 {
379+ return digest , fmt .Errorf ("more than one match for the digest %q" , options .Digest )
380+ }
381+ digest = l .Digest
382+ }
383+ if len (options .Title ) > 0 {
384+ if val , ok := l .Annotations [specV1 .AnnotationTitle ]; ok &&
385+ val == options .Title {
386+ if len (digest .String ()) > 0 {
387+ return digest , fmt .Errorf ("more than one match for the title %q" , options .Title )
388+ }
389+ digest = l .Digest
390+ }
391+ }
392+ }
393+ if len (digest .String ()) == 0 {
394+ if len (options .Title ) > 0 {
395+ return digest , fmt .Errorf ("no blob with the title %q" , options .Title )
396+ }
397+ return digest , fmt .Errorf ("no blob with the digest %q" , options .Digest )
398+ }
399+ return digest , nil
400+ }
401+
402+ func copyImageBlobToFile (ctx context.Context , imgSrc types.ImageSource , digest digest.Digest , target string ) error {
403+ src , _ , err := imgSrc .GetBlob (ctx , types.BlobInfo {Digest : digest }, nil )
404+ if err != nil {
405+ return fmt .Errorf ("failed to get artifact file: %w" , err )
406+ }
407+ defer src .Close ()
408+ dest , err := os .Create (target )
409+ if err != nil {
410+ return fmt .Errorf ("failed to create target file: %w" , err )
411+ }
412+ defer dest .Close ()
413+
414+ // TODO use reflink is possible
415+ _ , err = io .Copy (dest , src )
416+ return err
417+ }
418+
257419// readIndex is currently unused but I want to keep this around until
258420// the artifact code is more mature.
259421func (as ArtifactStore ) readIndex () (* specV1.Index , error ) { //nolint:unused
0 commit comments