github.com/anchore/syft@v1.38.2/syft/source/snapsource/snap.go (about) 1 package snapsource 2 3 import ( 4 "context" 5 "crypto" 6 "fmt" 7 "os" 8 "path" 9 "path/filepath" 10 "strings" 11 12 "github.com/spf13/afero" 13 14 stereoFile "github.com/anchore/stereoscope/pkg/file" 15 "github.com/anchore/syft/internal/bus" 16 intFile "github.com/anchore/syft/internal/file" 17 "github.com/anchore/syft/internal/log" 18 "github.com/anchore/syft/syft/event/monitor" 19 "github.com/anchore/syft/syft/file" 20 ) 21 22 type snapFile struct { 23 Path string 24 Digests []file.Digest 25 MimeType string 26 Cleanup func() error 27 } 28 29 type remoteSnap struct { 30 snapIdentity 31 URL string 32 } 33 34 type snapIdentity struct { 35 Name string 36 Channel string 37 Architecture string 38 } 39 40 func (s snapIdentity) String() string { 41 parts := []string{s.Name} 42 43 if s.Channel != "" { 44 parts = append(parts, fmt.Sprintf("@%s", s.Channel)) 45 } 46 47 if s.Architecture != "" { 48 parts = append(parts, fmt.Sprintf(" (%s)", s.Architecture)) 49 } 50 51 return strings.Join(parts, "") 52 } 53 54 func getRemoteSnapFile(ctx context.Context, fs afero.Fs, getter intFile.Getter, cfg Config) (*snapFile, error) { 55 if cfg.Request == "" { 56 return nil, fmt.Errorf("invalid request: %q", cfg.Request) 57 } 58 59 var architecture string 60 if cfg.Platform != nil { 61 architecture = cfg.Platform.Architecture 62 } 63 64 info, err := resolveRemoteSnap(cfg.Request, architecture) 65 if err != nil { 66 return nil, err 67 } 68 69 return newSnapFileFromRemote(ctx, fs, cfg, getter, info) 70 } 71 72 func newSnapFileFromRemote(ctx context.Context, fs afero.Fs, cfg Config, getter intFile.Getter, info *remoteSnap) (*snapFile, error) { 73 t, err := afero.TempDir(fs, "", "syft-snap-") 74 if err != nil { 75 return nil, fmt.Errorf("failed to create temp directory: %w", err) 76 } 77 78 snapFilePath := path.Join(t, path.Base(info.URL)) 79 err = downloadSnap(getter, info, snapFilePath) 80 if err != nil { 81 return nil, fmt.Errorf("failed to download snap file: %w", err) 82 } 83 84 closer := func() error { 85 return fs.RemoveAll(t) 86 } 87 88 mimeType, digests, err := getSnapFileInfo(ctx, fs, snapFilePath, cfg.DigestAlgorithms) 89 if err != nil { 90 return nil, err 91 } 92 93 return &snapFile{ 94 Path: snapFilePath, 95 Digests: digests, 96 MimeType: mimeType, 97 Cleanup: closer, 98 }, nil 99 } 100 101 func newSnapFromFile(ctx context.Context, fs afero.Fs, cfg Config) (*snapFile, error) { 102 var architecture string 103 if cfg.Platform != nil { 104 architecture = cfg.Platform.Architecture 105 } 106 107 if architecture != "" { 108 return nil, fmt.Errorf("architecture cannot be specified for local snap files: %q", cfg.Request) 109 } 110 111 absPath, err := filepath.Abs(cfg.Request) 112 if err != nil { 113 return nil, fmt.Errorf("unable to get absolute path of snap: %w", err) 114 } 115 116 mimeType, digests, err := getSnapFileInfo(ctx, fs, absPath, cfg.DigestAlgorithms) 117 if err != nil { 118 return nil, err 119 } 120 121 return &snapFile{ 122 Path: absPath, 123 Digests: digests, 124 MimeType: mimeType, 125 // note: we have no closer since this is the user's file (never delete it) 126 }, nil 127 } 128 129 func getSnapFileInfo(ctx context.Context, fs afero.Fs, path string, hashes []crypto.Hash) (string, []file.Digest, error) { 130 fileMeta, err := fs.Stat(path) 131 if err != nil { 132 return "", nil, fmt.Errorf("unable to stat path=%q: %w", path, err) 133 } 134 135 if fileMeta.IsDir() { 136 return "", nil, fmt.Errorf("given path is a directory, not a snap file: %q", path) 137 } 138 139 fh, err := fs.Open(path) 140 if err != nil { 141 return "", nil, fmt.Errorf("unable to open file=%q: %w", path, err) 142 } 143 defer fh.Close() 144 145 mimeType := stereoFile.MIMEType(fh) 146 if !isSquashFSFile(mimeType, path) { 147 return "", nil, fmt.Errorf("not a valid squashfs/snap file: %q (mime-type=%q)", path, mimeType) 148 } 149 150 var digests []file.Digest 151 if len(hashes) > 0 { 152 if _, err := fh.Seek(0, 0); err != nil { 153 return "", nil, fmt.Errorf("unable to reset file position: %w", err) 154 } 155 156 digests, err = intFile.NewDigestsFromFile(ctx, fh, hashes) 157 if err != nil { 158 return "", nil, fmt.Errorf("unable to calculate digests for file=%q: %w", path, err) 159 } 160 } 161 162 return mimeType, digests, nil 163 } 164 165 // resolveRemoteSnap parses a snap request and returns the appropriate path or URL 166 // The request can be: 167 // - A snap name (e.g., "etcd") 168 // - A snap name with channel (e.g., "etcd@beta" or "etcd@2.3/stable") 169 func resolveRemoteSnap(request, architecture string) (*remoteSnap, error) { 170 if architecture == "" { 171 architecture = defaultArchitecture 172 } 173 174 snapName, channel := parseSnapRequest(request) 175 176 id := snapIdentity{ 177 Name: snapName, 178 Channel: channel, 179 Architecture: architecture, 180 } 181 182 client := newSnapcraftClient() 183 184 downloadURL, err := client.GetSnapDownloadURL(id) 185 if err != nil { 186 return nil, err 187 } 188 189 log.WithFields("url", downloadURL, "name", snapName, "channel", channel, "architecture", architecture).Debugf("snap resolved") 190 191 return &remoteSnap{ 192 snapIdentity: id, 193 URL: downloadURL, 194 }, nil 195 } 196 197 // parseSnapRequest parses a snap request into name and channel 198 // Examples: 199 // - "etcd" -> name="etcd", channel="stable" (default) 200 // - "etcd@beta" -> name="etcd", channel="beta" 201 // - "etcd@2.3/stable" -> name="etcd", channel="2.3/stable" 202 func parseSnapRequest(request string) (name, channel string) { 203 parts := strings.SplitN(request, "@", 2) 204 name = parts[0] 205 206 if len(parts) == 2 { 207 channel = parts[1] 208 } 209 210 if channel == "" { 211 channel = defaultChannel 212 } 213 214 return name, channel 215 } 216 217 func downloadSnap(getter intFile.Getter, info *remoteSnap, dest string) error { 218 log.WithFields("url", info.URL, "destination", dest).Debug("downloading snap file") 219 220 prog := bus.StartPullSourceTask(monitor.GenericTask{ 221 Title: monitor.Title{ 222 Default: "Download snap", 223 WhileRunning: "Downloading snap", 224 OnSuccess: "Downloaded snap", 225 }, 226 HideOnSuccess: false, 227 HideStageOnSuccess: true, 228 ID: "", 229 ParentID: "", 230 Context: info.String(), 231 }, -1, "") 232 233 if err := getter.GetFile(dest, info.URL, prog.Manual); err != nil { 234 prog.SetError(err) 235 return fmt.Errorf("failed to download snap file at %q: %w", info.URL, err) 236 } 237 238 prog.SetCompleted() 239 return nil 240 } 241 242 // fileExists checks if a file exists and is not a directory 243 func fileExists(fs afero.Fs, path string) bool { 244 info, err := fs.Stat(path) 245 if os.IsNotExist(err) { 246 return false 247 } 248 return err == nil && !info.IsDir() 249 }