Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: validate input media type for oras.PackManifest #574

Merged
merged 5 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions errdef/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var (
ErrAlreadyExists = errors.New("already exists")
ErrInvalidDigest = errors.New("invalid digest")
ErrInvalidReference = errors.New("invalid reference")
ErrInvalidMediaType = errors.New("invalid media type")
ErrMissingReference = errors.New("missing reference")
ErrNotFound = errors.New("not found")
ErrSizeExceedsLimit = errors.New("size exceeds limit")
Expand Down
4 changes: 2 additions & 2 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func Example_pushFilesToRemoteRepository() {
ctx := context.Background()

// 1. Add files to the file store
mediaType := "example/file"
mediaType := "application/vnd.test.file"
fileNames := []string{"/tmp/myfile"}
fileDescriptors := make([]v1.Descriptor, 0, len(fileNames))
for _, name := range fileNames {
Expand All @@ -124,7 +124,7 @@ func Example_pushFilesToRemoteRepository() {
}

// 2. Pack the files and tag the packed manifest
artifactType := "example/files"
artifactType := "application/vnd.test.artifact"
opts := oras.PackManifestOptions{
Layers: fileDescriptors,
}
Expand Down
87 changes: 63 additions & 24 deletions pack.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"encoding/json"
"errors"
"fmt"
"regexp"
"time"

specs "github.com/opencontainers/image-spec/specs-go"
Expand Down Expand Up @@ -95,6 +96,12 @@ type PackManifestOptions struct {
ConfigAnnotations map[string]string
}

// mediaTypeRegexp checks the format of media types.
// References:
// - https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/schema/defs-descriptor.json#L7
// - https://datatracker.ietf.org/doc/html/rfc6838#section-4.2
var mediaTypeRegexp = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$`)

// PackManifest generates an OCI Image Manifest based on the given parameters
// and pushes the packed manifest to a content storage using pusher. The version
// of the manifest to be packed is determined by packManifestVersion
Expand All @@ -108,6 +115,8 @@ type PackManifestOptions struct {
// "application/vnd.unknown.config.v1+json" will be used.
// if opts.ConfigDescriptor is NOT nil, artifactType will be ignored.
//
// artifactType and opts.ConfigDescriptor.MediaType MUST comply with RFC 6838.
//
// If succeeded, returns a descriptor of the packed manifest.
func PackManifest(ctx context.Context, pusher content.Pusher, packManifestVersion PackManifestVersion, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) {
switch packManifestVersion {
Expand Down Expand Up @@ -190,28 +199,30 @@ func packArtifact(ctx context.Context, pusher content.Pusher, artifactType strin

// packManifestV1_0 packs an image manifest defined in image-spec v1.0.2.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md
func packManifestV1_0(ctx context.Context, pusher content.Pusher, configMediaType string, opts PackManifestOptions) (ocispec.Descriptor, error) {
func packManifestV1_0(ctx context.Context, pusher content.Pusher, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) {
if opts.Subject != nil {
return ocispec.Descriptor{}, fmt.Errorf("subject is not supported for manifest version %v: %w", PackManifestVersion1_0, errdef.ErrUnsupported)
}
if configMediaType == "" {
configMediaType = MediaTypeUnknownConfig
}

// prepare config
var configDesc ocispec.Descriptor
if opts.ConfigDescriptor != nil {
if err := validateMediaType(opts.ConfigDescriptor.MediaType); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("invalid config mediaType format: %s: %w", opts.ConfigDescriptor.MediaType, err)
}
configDesc = *opts.ConfigDescriptor
} else {
// Use an empty JSON object here, because some registries may not accept
// empty config blob.
// As of September 2022, GAR is known to return 400 on empty blob upload.
// See https://github.com/oras-project/oras-go/issues/294 for details.
configBytes := []byte("{}")
configDesc = content.NewDescriptorFromBytes(configMediaType, configBytes)
configDesc.Annotations = opts.ConfigAnnotations
// push config
if err := pushIfNotExist(ctx, pusher, configDesc, configBytes); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err)
if artifactType != "" {
if err := validateMediaType(artifactType); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("invalid artifactType format: %s: %w", artifactType, err)
}
} else {
artifactType = MediaTypeUnknownConfig
}
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved
var err error
configDesc, err = pushCustomConfig(ctx, pusher, artifactType, opts.ConfigAnnotations)
if err != nil {
return ocispec.Descriptor{}, err
}
}

Expand Down Expand Up @@ -242,20 +253,15 @@ func packManifestV1_1_RC2(ctx context.Context, pusher content.Pusher, configMedi
configMediaType = MediaTypeUnknownConfig
}

// prepare config
var configDesc ocispec.Descriptor
if opts.ConfigDescriptor != nil {
configDesc = *opts.ConfigDescriptor
} else {
// Use an empty JSON object here, because some registries may not accept
// empty config blob.
// As of September 2022, GAR is known to return 400 on empty blob upload.
// See https://github.com/oras-project/oras-go/issues/294 for details.
configBytes := []byte("{}")
configDesc = content.NewDescriptorFromBytes(configMediaType, configBytes)
configDesc.Annotations = opts.ConfigAnnotations
// push config
if err := pushIfNotExist(ctx, pusher, configDesc, configBytes); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err)
var err error
configDesc, err = pushCustomConfig(ctx, pusher, configMediaType, opts.ConfigAnnotations)
if err != nil {
return ocispec.Descriptor{}, err
}
}

Expand Down Expand Up @@ -286,10 +292,19 @@ func packManifestV1_1_RC4(ctx context.Context, pusher content.Pusher, artifactTy
// artifactType MUST be set when config.mediaType is set to the empty value
return ocispec.Descriptor{}, ErrMissingArtifactType
}
if artifactType != "" {
if err := validateMediaType(artifactType); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("invalid artifactType format: %s: %w", artifactType, err)
}
}

// prepare config
var emptyBlobExists bool
var configDesc ocispec.Descriptor
if opts.ConfigDescriptor != nil {
if err := validateMediaType(opts.ConfigDescriptor.MediaType); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("invalid config mediaType format: %s: %w", opts.ConfigDescriptor.MediaType, err)
}
configDesc = *opts.ConfigDescriptor
} else {
// use the empty descriptor for config
Expand Down Expand Up @@ -369,6 +384,22 @@ func pushManifest(ctx context.Context, pusher content.Pusher, manifest any, medi
return manifestDesc, nil
}

// pushCustomConfig generates and pushes a custom config blob.
func pushCustomConfig(ctx context.Context, pusher content.Pusher, mediaType string, annotations map[string]string) (ocispec.Descriptor, error) {
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved
// Use an empty JSON object here, because some registries may not accept
// empty config blob.
// As of September 2022, GAR is known to return 400 on empty blob upload.
// See https://github.com/oras-project/oras-go/issues/294 for details.
configBytes := []byte("{}")
configDesc := content.NewDescriptorFromBytes(mediaType, configBytes)
configDesc.Annotations = annotations
// push config
if err := pushIfNotExist(ctx, pusher, configDesc, configBytes); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err)
}
return configDesc, nil
}

// ensureAnnotationCreated ensures that annotationCreatedKey is in annotations,
// and that its value conforms to RFC 3339. Otherwise returns a new annotation
// map with annotationCreatedKey created.
Expand All @@ -392,3 +423,11 @@ func ensureAnnotationCreated(annotations map[string]string, annotationCreatedKey
copied[annotationCreatedKey] = now.Format(time.RFC3339)
return copied, nil
}

// validateMediaType validates the format of mediaType.
func validateMediaType(mediaType string) error {
if !mediaTypeRegexp.MatchString(mediaType) {
return errdef.ErrInvalidMediaType
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved
}
return nil
}
Loading
Loading