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 }