mirror of
https://github.com/hauler-dev/hauler.git
synced 2026-08-19 04:16:27 +00:00
126 lines
3.8 KiB
Go
126 lines
3.8 KiB
Go
package mapper
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
ccontent "github.com/containerd/containerd/v2/core/content"
|
|
"github.com/containerd/containerd/v2/core/remotes"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/pkg/errors"
|
|
"hauler.dev/go/hauler/v2/pkg/content"
|
|
)
|
|
|
|
// NewMapperFileStore creates a new file store that uses mapper functions for each detected descriptor.
|
|
//
|
|
// This extends content.OCI, and differs in that it allows much more functionality into how each descriptor is written.
|
|
func NewMapperFileStore(root string, mapper map[string]Fn) (*store, error) {
|
|
fs, err := content.NewOCI(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &store{
|
|
OCI: fs,
|
|
mapper: mapper,
|
|
}, nil
|
|
}
|
|
|
|
func (s *store) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) {
|
|
var tag, hash string
|
|
parts := strings.SplitN(ref, "@", 2)
|
|
if len(parts) > 0 {
|
|
tag = parts[0]
|
|
}
|
|
if len(parts) > 1 {
|
|
hash = parts[1]
|
|
}
|
|
return &pusher{
|
|
store: s.OCI,
|
|
tag: tag,
|
|
ref: hash,
|
|
mapper: s.mapper,
|
|
}, nil
|
|
}
|
|
|
|
type store struct {
|
|
*content.OCI
|
|
mapper map[string]Fn
|
|
}
|
|
|
|
func (s *pusher) Push(ctx context.Context, desc ocispec.Descriptor) (ccontent.Writer, error) {
|
|
// For manifests and indexes (which have AnnotationRefName), discard them
|
|
// They're metadata and don't need to be extracted
|
|
if _, ok := content.ResolveName(desc); ok {
|
|
// Discard manifests/indexes, they're just metadata
|
|
return content.NewIoContentWriter(&nopCloser{io.Discard}, content.WithOutputHash(desc.Digest.String())), nil
|
|
}
|
|
|
|
// Check if this descriptor has a mapper for its media type
|
|
mapperFn, hasMapper := s.mapper[desc.MediaType]
|
|
if !hasMapper {
|
|
// Fall back to catch-all sentinel, then discard
|
|
mapperFn, hasMapper = s.mapper[DefaultCatchAll]
|
|
}
|
|
if !hasMapper {
|
|
// No mapper for this media type, discard it (config blobs, etc.)
|
|
return content.NewIoContentWriter(&nopCloser{io.Discard}, content.WithOutputHash(desc.Digest.String())), nil
|
|
}
|
|
|
|
// Get the filename from the mapper function.
|
|
// An empty filename means the mapper explicitly declined this descriptor (e.g. a
|
|
// config blob that has no title annotation... treat it the same as no mapper.
|
|
filename, err := mapperFn(desc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if filename == "" {
|
|
return content.NewIoContentWriter(&nopCloser{io.Discard}, content.WithOutputHash(desc.Digest.String())), nil
|
|
}
|
|
|
|
// Get the destination directory and create the full path.
|
|
// Use absolute paths so the traversal check works even when destDir is relative (e.g. ".").
|
|
destDir, err := filepath.Abs(s.store.ResolvePath(""))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "resolving destination dir")
|
|
}
|
|
fullFileName := filepath.Join(destDir, filename)
|
|
|
|
// Guard against path traversal (e.g. "../"). filepath.Rel handles this
|
|
// correctly even when destDir is "/", unlike a plain prefix check.
|
|
rel, err := filepath.Rel(destDir, fullFileName)
|
|
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
|
return nil, fmt.Errorf("path_traversal_disallowed: %q resolves outside destination dir", filename)
|
|
}
|
|
|
|
// Create parent directories (e.g. when filename is "subdir/file.txt")
|
|
if err := os.MkdirAll(filepath.Dir(fullFileName), 0755); err != nil {
|
|
return nil, errors.Wrap(err, fmt.Sprintf("creating directory for %s", fullFileName))
|
|
}
|
|
|
|
// Create the file
|
|
f, err := os.OpenFile(fullFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, fmt.Sprintf("creating file %s", fullFileName))
|
|
}
|
|
|
|
w := content.NewIoContentWriter(f, content.WithOutputHash(desc.Digest.String()))
|
|
return w, nil
|
|
}
|
|
|
|
type nopCloser struct {
|
|
io.Writer
|
|
}
|
|
|
|
func (*nopCloser) Close() error { return nil }
|
|
|
|
type pusher struct {
|
|
store *content.OCI
|
|
tag string
|
|
ref string
|
|
mapper map[string]Fn
|
|
}
|