Skip to content

Commit

Permalink
feat: validate input media type for oras.PackManifest (#574)
Browse files Browse the repository at this point in the history
Resolves: #571
Signed-off-by: Lixia (Sylvia) Lei <lixlei@microsoft.com>
  • Loading branch information
Wwwsylvia committed Aug 24, 2023
1 parent 60da91b commit 47d028a
Show file tree
Hide file tree
Showing 4 changed files with 298 additions and 202 deletions.
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
85 changes: 61 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,28 @@ 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: %w", 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 == "" {
artifactType = MediaTypeUnknownConfig
} else if err := validateMediaType(artifactType); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("invalid artifactType format: %w", err)
}
var err error
configDesc, err = pushCustomEmptyConfig(ctx, pusher, artifactType, opts.ConfigAnnotations)
if err != nil {
return ocispec.Descriptor{}, err
}
}

Expand Down Expand Up @@ -242,20 +251,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 = pushCustomEmptyConfig(ctx, pusher, configMediaType, opts.ConfigAnnotations)
if err != nil {
return ocispec.Descriptor{}, err
}
}

Expand Down Expand Up @@ -286,10 +290,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: %w", 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: %w", err)
}
configDesc = *opts.ConfigDescriptor
} else {
// use the empty descriptor for config
Expand Down Expand Up @@ -369,6 +382,22 @@ func pushManifest(ctx context.Context, pusher content.Pusher, manifest any, medi
return manifestDesc, nil
}

// pushCustomEmptyConfig generates and pushes an empty config blob.
func pushCustomEmptyConfig(ctx context.Context, pusher content.Pusher, mediaType string, annotations map[string]string) (ocispec.Descriptor, error) {
// 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 +421,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 fmt.Errorf("%s: %w", mediaType, errdef.ErrInvalidMediaType)
}
return nil
}
Loading

0 comments on commit 47d028a

Please sign in to comment.