Files
syft/syft/internal/fileresolver/container_image_model.go
T
2026-07-15 17:14:03 -04:00

154 lines
5.2 KiB
Go

package fileresolver
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"github.com/anchore/syft/syft/file"
)
var _ file.Resolver = (*ContainerImageModel)(nil)
var _ file.OCIMediaTypeResolver = (*ContainerImageModel)(nil)
// LayerInfo holds information about an OCI model layer file stored on disk.
type LayerInfo struct {
TempPath string // Path to the temp file on disk
MediaType string // OCI media type of the layer
}
// ContainerImageModel is a file.Resolver implementation that provides access to
// model header and metadata data (GGUF and SafeTensors headers, the model config
// blob, and companion layers) fetched from OCI model artifacts via range-GET
// requests. This does not fetch the entire model from the registry, only a
// sliver of it.
type ContainerImageModel struct {
tempDir string // temp directory containing all layer files
layerFiles map[string]LayerInfo // digest -> layer info (temp path + media type)
locations map[string]file.Location // digest -> location
ref string // image reference the artifact was fetched with
}
// NewContainerImageModel creates a new resolver with the given temp directory
// and layer files. The ref is surfaced through the ImageReference method so
// catalogers can derive context-level naming hints from the artifact reference
// when the layer contents don't carry one.
func NewContainerImageModel(tempDir string, layerFiles map[string]LayerInfo, ref string) *ContainerImageModel {
// Create locations for all layer files
// Each location has RealPath="/", FileSystemID=digest, AccessPath="/"
locations := make(map[string]file.Location, len(layerFiles))
for digest := range layerFiles {
// Use NewVirtualLocationFromCoordinates with digest as FileSystemID
coords := file.NewCoordinates("/", digest)
locations[digest] = file.NewVirtualLocationFromCoordinates(coords, "/")
}
return &ContainerImageModel{
tempDir: tempDir,
layerFiles: layerFiles,
locations: locations,
ref: ref,
}
}
// ImageReference returns the image reference the artifact was fetched with.
func (r *ContainerImageModel) ImageReference() string {
return r.ref
}
// FilesByMediaType returns locations for layers matching the given media type patterns.
// Patterns support glob-style matching (e.g., "application/vnd.docker.ai*").
func (r *ContainerImageModel) FilesByMediaType(types ...string) ([]file.Location, error) {
var matches []file.Location
for digest, info := range r.layerFiles {
for _, pattern := range types {
matched, err := filepath.Match(pattern, info.MediaType)
if err != nil {
return nil, fmt.Errorf("invalid media type pattern %q: %w", pattern, err)
}
if matched {
if loc, ok := r.locations[digest]; ok {
matches = append(matches, loc)
}
break // Don't add the same location twice
}
}
}
return matches, nil
}
// FileContentsByLocation returns the contents of the file at the given location.
// The location's FileSystemID contains the layer digest, which is used to look up the temp file.
// This method is used as part of the content selection in the generic cataloger when locations
// are returned by searching for contents by media type.
func (r *ContainerImageModel) FileContentsByLocation(location file.Location) (io.ReadCloser, error) {
// Look up the temp file path using the digest stored in FileSystemID
digest := location.FileSystemID
info, ok := r.layerFiles[digest]
if !ok {
return nil, fmt.Errorf("no file found for digest %q", digest)
}
return os.Open(info.TempPath)
}
// FileMetadataByLocation returns metadata for the file at the given location.
func (r *ContainerImageModel) FileMetadataByLocation(_ file.Location) (m file.Metadata, err error) {
return m, nil
}
// HasPath checks if the given path exists in the resolver.
func (r *ContainerImageModel) HasPath(path string) bool {
// The virtual path is "/" for all files
if path == "/" && len(r.layerFiles) > 0 {
return true
}
return false
}
// FilesByPath returns locations for files matching the given paths.
func (r *ContainerImageModel) FilesByPath(_ ...string) ([]file.Location, error) {
return nil, nil
}
// FilesByGlob returns locations for files matching the given glob patterns.
func (r *ContainerImageModel) FilesByGlob(_ ...string) ([]file.Location, error) {
return nil, nil
}
// FilesByMIMEType returns locations for files with the given MIME types.
// This is not implemented for OCI model artifacts as we don't have MIME type detection.
func (r *ContainerImageModel) FilesByMIMEType(_ ...string) ([]file.Location, error) {
// Not implemented - OCI model artifacts don't have MIME type detection
return nil, nil
}
// RelativeFileByPath returns a file at the given path relative to the reference location.
// This is not applicable for OCI model artifacts.
func (r *ContainerImageModel) RelativeFileByPath(_ file.Location, _ string) *file.Location {
// Not implemented - no layer hierarchy in OCI model artifacts
return nil
}
// AllLocations returns all file locations in the resolver.
func (r *ContainerImageModel) AllLocations(ctx context.Context) <-chan file.Location {
ch := make(chan file.Location)
go func() {
defer close(ch)
for _, loc := range r.locations {
select {
case <-ctx.Done():
return
case ch <- loc:
}
}
}()
return ch
}