cuelang.org/go@v0.13.0/mod/modregistry/client.go (about)

     1  // Copyright 2023 CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package modregistry provides functionality for reading and writing
    16  // CUE modules from an OCI registry.
    17  //
    18  // WARNING: THIS PACKAGE IS EXPERIMENTAL.
    19  // ITS API MAY CHANGE AT ANY TIME.
    20  package modregistry
    21  
    22  import (
    23  	"archive/zip"
    24  	"bytes"
    25  	"context"
    26  	"encoding/json"
    27  	"errors"
    28  	"fmt"
    29  	"io"
    30  	"net/http"
    31  	"strings"
    32  
    33  	"cuelabs.dev/go/oci/ociregistry"
    34  	"cuelabs.dev/go/oci/ociregistry/ociref"
    35  	"cuelang.org/go/internal/mod/semver"
    36  	digest "github.com/opencontainers/go-digest"
    37  	specs "github.com/opencontainers/image-spec/specs-go"
    38  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    39  	"golang.org/x/sync/errgroup"
    40  
    41  	"cuelang.org/go/cue/ast"
    42  	"cuelang.org/go/mod/modfile"
    43  	"cuelang.org/go/mod/module"
    44  	"cuelang.org/go/mod/modzip"
    45  )
    46  
    47  var ErrNotFound = fmt.Errorf("module not found")
    48  
    49  // Client represents a OCI-registry-backed client that
    50  // provides a store for CUE modules.
    51  type Client struct {
    52  	resolver Resolver
    53  }
    54  
    55  // Resolver resolves module paths to a registry and a location
    56  // within that registry.
    57  type Resolver interface {
    58  	// ResolveToRegistry resolves a base module path (without a version)
    59  	// and optional version to the location for that path.
    60  	//
    61  	// If the version is empty, the Tag in the returned Location
    62  	// will hold the prefix that all versions of the module in its
    63  	// repository have. That prefix will be followed by the version
    64  	// itself.
    65  	//
    66  	// If there is no registry configured for the module, it returns
    67  	// an [ErrRegistryNotFound] error.
    68  	ResolveToRegistry(mpath, vers string) (RegistryLocation, error)
    69  }
    70  
    71  // ErrRegistryNotFound is returned by [Resolver.ResolveToRegistry]
    72  // when there is no registry configured for a module.
    73  var ErrRegistryNotFound = fmt.Errorf("no registry configured for module")
    74  
    75  // RegistryLocation holds a registry and a location within it
    76  // that a specific module (or set of versions for a module)
    77  // will be stored.
    78  type RegistryLocation struct {
    79  	// Registry holds the registry to use to access the module.
    80  	Registry ociregistry.Interface
    81  	// Repository holds the repository where the module is located.
    82  	Repository string
    83  	// Tag holds the tag for the module version. If an empty version
    84  	// was passed to Resolve, it holds the prefix shared by all
    85  	// version tags for the module.
    86  	Tag string
    87  }
    88  
    89  const (
    90  	moduleArtifactType  = "application/vnd.cue.module.v1+json"
    91  	moduleFileMediaType = "application/vnd.cue.modulefile.v1"
    92  	moduleAnnotation    = "works.cue.module"
    93  )
    94  
    95  // NewClient returns a new client that talks to the registry at the given
    96  // hostname.
    97  func NewClient(registry ociregistry.Interface) *Client {
    98  	return &Client{
    99  		resolver: singleResolver{registry},
   100  	}
   101  }
   102  
   103  // NewClientWithResolver returns a new client that uses the given
   104  // resolver to decide which registries to fetch from or push to.
   105  func NewClientWithResolver(resolver Resolver) *Client {
   106  	return &Client{
   107  		resolver: resolver,
   108  	}
   109  }
   110  
   111  // Mirror ensures that the given module and its component parts
   112  // are present and identical in both src and dst.
   113  func (src *Client) Mirror(ctx context.Context, dst *Client, mv module.Version) error {
   114  	m, err := src.GetModule(ctx, mv)
   115  	if err != nil {
   116  		return err
   117  	}
   118  	dstLoc, err := dst.resolve(mv)
   119  	if err != nil {
   120  		return fmt.Errorf("cannot resolve module in destination: %w", err)
   121  	}
   122  
   123  	// TODO ideally this parallelism would respect parallelism limits
   124  	// on whatever is calling the function too rather than just doing
   125  	// all uploads in parallel.
   126  	var g errgroup.Group
   127  	// TODO for desc := range manifestRefs(m.manifest)
   128  	manifestRefs(m.manifest)(func(desc ociregistry.Descriptor) bool {
   129  		g.Go(func() error {
   130  			return mirrorBlob(ctx, m.loc, dstLoc, desc)
   131  		})
   132  		return true
   133  	})
   134  	if err := g.Wait(); err != nil {
   135  		return err
   136  	}
   137  	// We've uploaded all the blobs referenced by the manifest; now
   138  	// we can upload the manifest itself.
   139  	if _, err := dstLoc.Registry.ResolveManifest(ctx, dstLoc.Repository, m.manifestDigest); err == nil {
   140  		return nil
   141  	}
   142  	if _, err := dstLoc.Registry.PushManifest(ctx, dstLoc.Repository, dstLoc.Tag, m.manifestContents, ocispec.MediaTypeImageManifest); err != nil {
   143  		return nil
   144  	}
   145  	return nil
   146  }
   147  
   148  func mirrorBlob(ctx context.Context, srcLoc, dstLoc RegistryLocation, desc ocispec.Descriptor) error {
   149  	desc1, err := dstLoc.Registry.ResolveBlob(ctx, dstLoc.Repository, desc.Digest)
   150  	if err == nil {
   151  		// Blob already exists in destination. Check that its size agrees.
   152  		if desc1.Size != desc.Size {
   153  			return fmt.Errorf("destination size (%d) does not agree with source size (%d) in blob %v", desc1.Size, desc.Size, desc.Digest)
   154  		}
   155  		return nil
   156  	}
   157  	r, err := srcLoc.Registry.GetBlob(ctx, srcLoc.Repository, desc.Digest)
   158  	if err != nil {
   159  		return err
   160  	}
   161  	defer r.Close()
   162  	if _, err := dstLoc.Registry.PushBlob(ctx, dstLoc.Repository, desc, r); err != nil {
   163  		return err
   164  	}
   165  	return nil
   166  }
   167  
   168  // GetModule returns the module instance for the given version.
   169  // It returns an error that satisfies [errors.Is]([ErrNotFound]) if the
   170  // module is not present in the store at this version.
   171  func (c *Client) GetModule(ctx context.Context, m module.Version) (*Module, error) {
   172  	loc, err := c.resolve(m)
   173  	if err != nil {
   174  		if errors.Is(err, ErrRegistryNotFound) {
   175  			return nil, ErrNotFound
   176  		}
   177  		return nil, err
   178  	}
   179  	rd, err := loc.Registry.GetTag(ctx, loc.Repository, loc.Tag)
   180  	if err != nil {
   181  		if isNotExist(err) {
   182  			return nil, fmt.Errorf("module %v: %w", m, ErrNotFound)
   183  		}
   184  		return nil, fmt.Errorf("module %v: %w", m, err)
   185  	}
   186  	defer rd.Close()
   187  	data, err := io.ReadAll(rd)
   188  	if err != nil {
   189  		return nil, err
   190  	}
   191  
   192  	return c.GetModuleWithManifest(m, data, rd.Descriptor().MediaType)
   193  }
   194  
   195  // GetModuleWithManifest returns a module instance given
   196  // the top level manifest contents, without querying its tag.
   197  // It assumes that the module will be tagged with the given version.
   198  func (c *Client) GetModuleWithManifest(m module.Version, contents []byte, mediaType string) (*Module, error) {
   199  	loc, err := c.resolve(m)
   200  	if err != nil {
   201  		// Note: don't return [ErrNotFound] here because if we've got the
   202  		// manifest we should be pretty sure that the module actually
   203  		// exists, so it's a harder error than if we're getting the module
   204  		// by tag.
   205  		return nil, err
   206  	}
   207  
   208  	manifest, err := unmarshalManifest(contents, mediaType)
   209  	if err != nil {
   210  		return nil, fmt.Errorf("module %v: %v", m, err)
   211  	}
   212  	if !isModule(manifest) {
   213  		return nil, fmt.Errorf("%v does not resolve to a manifest (media type is %q)", m, mediaType)
   214  	}
   215  	// TODO check type of manifest too.
   216  	if n := len(manifest.Layers); n != 2 {
   217  		return nil, fmt.Errorf("module manifest should refer to exactly two blobs, but got %d", n)
   218  	}
   219  	if !isModuleFile(manifest.Layers[1]) {
   220  		return nil, fmt.Errorf("unexpected media type %q for module file blob", manifest.Layers[1].MediaType)
   221  	}
   222  	// TODO check that the other blobs are of the expected type (application/zip).
   223  	return &Module{
   224  		client:           c,
   225  		loc:              loc,
   226  		version:          m,
   227  		manifest:         *manifest,
   228  		manifestContents: contents,
   229  		manifestDigest:   digest.FromBytes(contents),
   230  	}, nil
   231  }
   232  
   233  // ModuleVersions returns all the versions for the module with the given path
   234  // sorted in semver order.
   235  // If m has a major version suffix, only versions with that major version will
   236  // be returned.
   237  func (c *Client) ModuleVersions(ctx context.Context, m string) (_req []string, _err0 error) {
   238  	mpath, major, hasMajor := ast.SplitPackageVersion(m)
   239  	loc, err := c.resolver.ResolveToRegistry(mpath, "")
   240  	if err != nil {
   241  		if errors.Is(err, ErrRegistryNotFound) {
   242  			return nil, nil
   243  		}
   244  		return nil, err
   245  	}
   246  	versions := []string{}
   247  	if !ociref.IsValidRepository(loc.Repository) {
   248  		// If it's not a valid repository, it can't be used in an OCI
   249  		// request, so return an empty slice rather than the
   250  		// "invalid OCI request" error that a registry can return.
   251  		return nil, nil
   252  	}
   253  	// Note: do not use c.repoName because that always expects
   254  	// a module path with a major version.
   255  	iter := loc.Registry.Tags(ctx, loc.Repository, "")
   256  	var _err error
   257  	iter(func(tag string, err error) bool {
   258  		if err != nil {
   259  			_err = err
   260  			return false
   261  		}
   262  		vers, ok := strings.CutPrefix(tag, loc.Tag)
   263  		if !ok || !semver.IsValid(vers) {
   264  			return true
   265  		}
   266  		if !hasMajor || semver.Major(vers) == major {
   267  			versions = append(versions, vers)
   268  		}
   269  		return true
   270  	})
   271  	if _err != nil && !isNotExist(_err) {
   272  		return nil, fmt.Errorf("module %v: %w", m, _err)
   273  	}
   274  	semver.Sort(versions)
   275  	return versions, nil
   276  }
   277  
   278  // checkedModule represents module content that has passed the same
   279  // checks made by [Client.PutModule]. The caller should not mutate
   280  // any of the values returned by its methods.
   281  type checkedModule struct {
   282  	mv             module.Version
   283  	blobr          io.ReaderAt
   284  	size           int64
   285  	zipr           *zip.Reader
   286  	modFile        *modfile.File
   287  	modFileContent []byte
   288  }
   289  
   290  // putCheckedModule is like [Client.PutModule] except that it allows the
   291  // caller to do some additional checks (see [CheckModule] for more info).
   292  func (c *Client) putCheckedModule(ctx context.Context, m *checkedModule, meta *Metadata) error {
   293  	var annotations map[string]string
   294  	if meta != nil {
   295  		annotations0, err := meta.annotations()
   296  		if err != nil {
   297  			return fmt.Errorf("invalid metadata: %v", err)
   298  		}
   299  		annotations = annotations0
   300  	}
   301  	loc, err := c.resolve(m.mv)
   302  	if err != nil {
   303  		return err
   304  	}
   305  	selfDigest, err := digest.FromReader(io.NewSectionReader(m.blobr, 0, m.size))
   306  	if err != nil {
   307  		return fmt.Errorf("cannot read module zip file: %v", err)
   308  	}
   309  	// Upload the actual module's content
   310  	// TODO should we use a custom media type for this?
   311  	configDesc, err := c.scratchConfig(ctx, loc, moduleArtifactType)
   312  	if err != nil {
   313  		return fmt.Errorf("cannot make scratch config: %v", err)
   314  	}
   315  	manifest := &ocispec.Manifest{
   316  		Versioned: specs.Versioned{
   317  			SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
   318  		},
   319  		MediaType: ocispec.MediaTypeImageManifest,
   320  		Config:    configDesc,
   321  		// One for self, one for module file.
   322  		Layers: []ocispec.Descriptor{{
   323  			Digest:    selfDigest,
   324  			MediaType: "application/zip",
   325  			Size:      m.size,
   326  		}, {
   327  			Digest:    digest.FromBytes(m.modFileContent),
   328  			MediaType: moduleFileMediaType,
   329  			Size:      int64(len(m.modFileContent)),
   330  		}},
   331  		Annotations: annotations,
   332  	}
   333  
   334  	if _, err := loc.Registry.PushBlob(ctx, loc.Repository, manifest.Layers[0], io.NewSectionReader(m.blobr, 0, m.size)); err != nil {
   335  		return fmt.Errorf("cannot push module contents: %v", err)
   336  	}
   337  	if _, err := loc.Registry.PushBlob(ctx, loc.Repository, manifest.Layers[1], bytes.NewReader(m.modFileContent)); err != nil {
   338  		return fmt.Errorf("cannot push cue.mod/module.cue contents: %v", err)
   339  	}
   340  	manifestData, err := json.Marshal(manifest)
   341  	if err != nil {
   342  		return fmt.Errorf("cannot marshal manifest: %v", err)
   343  	}
   344  	if _, err := loc.Registry.PushManifest(ctx, loc.Repository, loc.Tag, manifestData, ocispec.MediaTypeImageManifest); err != nil {
   345  		return fmt.Errorf("cannot tag %v: %v", m.mv, err)
   346  	}
   347  	return nil
   348  }
   349  
   350  // PutModule puts a module whose contents are held as a zip archive inside f.
   351  // It assumes all the module dependencies are correctly resolved and present
   352  // inside the cue.mod/module.cue file.
   353  func (c *Client) PutModule(ctx context.Context, m module.Version, r io.ReaderAt, size int64) error {
   354  	return c.PutModuleWithMetadata(ctx, m, r, size, nil)
   355  }
   356  
   357  // PutModuleWithMetadata is like [Client.PutModule] except that it also
   358  // includes the given metadata inside the module's manifest.
   359  // If meta is nil, no metadata will be included, otherwise
   360  // all fields in meta must be valid and non-empty.
   361  func (c *Client) PutModuleWithMetadata(ctx context.Context, m module.Version, r io.ReaderAt, size int64, meta *Metadata) error {
   362  	cm, err := checkModule(m, r, size)
   363  	if err != nil {
   364  		return err
   365  	}
   366  	return c.putCheckedModule(ctx, cm, meta)
   367  }
   368  
   369  // checkModule checks a module's zip file before uploading it.
   370  // This does the same checks that [Client.PutModule] does, so
   371  // can be used to avoid doing duplicate work when an uploader
   372  // wishes to do more checks that are implemented by that method.
   373  //
   374  // Note that the returned [CheckedModule] value contains r, so will
   375  // be invalidated if r is closed.
   376  func checkModule(m module.Version, blobr io.ReaderAt, size int64) (*checkedModule, error) {
   377  	zipr, modf, _, err := modzip.CheckZip(m, blobr, size)
   378  	if err != nil {
   379  		return nil, fmt.Errorf("module zip file check failed: %v", err)
   380  	}
   381  	modFileContent, mf, err := checkModFile(m, modf)
   382  	if err != nil {
   383  		return nil, fmt.Errorf("module.cue file check failed: %v", err)
   384  	}
   385  	return &checkedModule{
   386  		mv:             m,
   387  		blobr:          blobr,
   388  		size:           size,
   389  		zipr:           zipr,
   390  		modFile:        mf,
   391  		modFileContent: modFileContent,
   392  	}, nil
   393  }
   394  
   395  func checkModFile(m module.Version, f *zip.File) ([]byte, *modfile.File, error) {
   396  	r, err := f.Open()
   397  	if err != nil {
   398  		return nil, nil, err
   399  	}
   400  	defer r.Close()
   401  	// TODO check max size?
   402  	data, err := io.ReadAll(r)
   403  	if err != nil {
   404  		return nil, nil, err
   405  	}
   406  	mf, err := modfile.Parse(data, f.Name)
   407  	if err != nil {
   408  		return nil, nil, err
   409  	}
   410  	if mf.QualifiedModule() != m.Path() {
   411  		return nil, nil, fmt.Errorf("module path %q found in %s does not match module path being published %q", mf.QualifiedModule(), f.Name, m.Path())
   412  	}
   413  	wantMajor := semver.Major(m.Version())
   414  	if major := mf.MajorVersion(); major != wantMajor {
   415  		// This can't actually happen because the zip checker checks the major version
   416  		// that's being published to, so the above path check also implicitly checks that.
   417  		return nil, nil, fmt.Errorf("major version %q found in %s does not match version being published %q", major, f.Name, m.Version())
   418  	}
   419  	// Check that all dependency versions look valid.
   420  	for modPath, dep := range mf.Deps {
   421  		_, err := module.NewVersion(modPath, dep.Version)
   422  		if err != nil {
   423  			return nil, nil, fmt.Errorf("invalid dependency: %v @ %v", modPath, dep.Version)
   424  		}
   425  	}
   426  	return data, mf, nil
   427  }
   428  
   429  // Module represents a CUE module instance.
   430  type Module struct {
   431  	client           *Client
   432  	loc              RegistryLocation
   433  	version          module.Version
   434  	manifest         ocispec.Manifest
   435  	manifestContents []byte
   436  	manifestDigest   ociregistry.Digest
   437  }
   438  
   439  func (m *Module) Version() module.Version {
   440  	return m.version
   441  }
   442  
   443  // ModuleFile returns the contents of the cue.mod/module.cue file.
   444  func (m *Module) ModuleFile(ctx context.Context) ([]byte, error) {
   445  	r, err := m.loc.Registry.GetBlob(ctx, m.loc.Repository, m.manifest.Layers[1].Digest)
   446  	if err != nil {
   447  		return nil, err
   448  	}
   449  	defer r.Close()
   450  	return io.ReadAll(r)
   451  }
   452  
   453  // Metadata returns the metadata associated with the module.
   454  // If there is none, it returns (nil, nil).
   455  func (m *Module) Metadata() (*Metadata, error) {
   456  	return newMetadataFromAnnotations(m.manifest.Annotations)
   457  }
   458  
   459  // GetZip returns a reader that can be used to read the contents of the zip
   460  // archive containing the module files. The reader should be closed after use,
   461  // and the contents should not be assumed to be correct until the close
   462  // error has been checked.
   463  func (m *Module) GetZip(ctx context.Context) (io.ReadCloser, error) {
   464  	return m.loc.Registry.GetBlob(ctx, m.loc.Repository, m.manifest.Layers[0].Digest)
   465  }
   466  
   467  // ManifestDigest returns the digest of the manifest representing
   468  // the module.
   469  func (m *Module) ManifestDigest() ociregistry.Digest {
   470  	return m.manifestDigest
   471  }
   472  
   473  func (c *Client) resolve(m module.Version) (RegistryLocation, error) {
   474  	loc, err := c.resolver.ResolveToRegistry(m.BasePath(), m.Version())
   475  	if err != nil {
   476  		return RegistryLocation{}, err
   477  	}
   478  	if loc.Registry == nil {
   479  		return RegistryLocation{}, fmt.Errorf("module %v unexpectedly resolved to nil registry", m)
   480  	}
   481  	if loc.Repository == "" {
   482  		return RegistryLocation{}, fmt.Errorf("module %v unexpectedly resolved to empty location", m)
   483  	}
   484  	if loc.Tag == "" {
   485  		return RegistryLocation{}, fmt.Errorf("module %v unexpectedly resolved to empty tag", m)
   486  	}
   487  	return loc, nil
   488  }
   489  
   490  func unmarshalManifest(data []byte, mediaType string) (*ociregistry.Manifest, error) {
   491  	if !isJSON(mediaType) {
   492  		return nil, fmt.Errorf("expected JSON media type but %q does not look like JSON", mediaType)
   493  	}
   494  	var m ociregistry.Manifest
   495  	if err := json.Unmarshal(data, &m); err != nil {
   496  		return nil, fmt.Errorf("cannot decode %s content as manifest: %v", mediaType, err)
   497  	}
   498  	return &m, nil
   499  }
   500  
   501  func isNotExist(err error) bool {
   502  	if errors.Is(err, ociregistry.ErrNameUnknown) ||
   503  		errors.Is(err, ociregistry.ErrNameInvalid) {
   504  		return true
   505  	}
   506  	// A 403 error might have been sent as a response
   507  	// without explicitly including a "denied" error code.
   508  	// We treat this as a "not found" error because there's
   509  	// nothing the user can do about it.
   510  	//
   511  	// Also, some registries return an invalid error code with a 404
   512  	// response (see https://cuelang.org/issue/2982), so it
   513  	// seems reasonable to treat that as a non-found error too.
   514  	if herr := ociregistry.HTTPError(nil); errors.As(err, &herr) {
   515  		statusCode := herr.StatusCode()
   516  		return statusCode == http.StatusForbidden ||
   517  			statusCode == http.StatusNotFound
   518  	}
   519  	return false
   520  }
   521  
   522  func isModule(m *ocispec.Manifest) bool {
   523  	// TODO check m.ArtifactType too when that's defined?
   524  	// See https://github.com/opencontainers/image-spec/blob/main/manifest.md#image-manifest-property-descriptions
   525  	return m.Config.MediaType == moduleArtifactType
   526  }
   527  
   528  func isModuleFile(desc ocispec.Descriptor) bool {
   529  	return desc.ArtifactType == moduleFileMediaType ||
   530  		desc.MediaType == moduleFileMediaType
   531  }
   532  
   533  // isJSON reports whether the given media type has JSON as an underlying encoding.
   534  // TODO this is a guess. There's probably a more correct way to do it.
   535  func isJSON(mediaType string) bool {
   536  	return strings.HasSuffix(mediaType, "+json") || strings.HasSuffix(mediaType, "/json")
   537  }
   538  
   539  // scratchConfig returns a dummy configuration consisting only of the
   540  // two-byte configuration {}.
   541  // https://github.com/opencontainers/image-spec/blob/main/manifest.md#example-of-a-scratch-config-or-layer-descriptor
   542  func (c *Client) scratchConfig(ctx context.Context, loc RegistryLocation, mediaType string) (ocispec.Descriptor, error) {
   543  	// TODO check if it exists already to avoid push?
   544  	content := []byte("{}")
   545  	desc := ocispec.Descriptor{
   546  		Digest:    digest.FromBytes(content),
   547  		MediaType: mediaType,
   548  		Size:      int64(len(content)),
   549  	}
   550  	if _, err := loc.Registry.PushBlob(ctx, loc.Repository, desc, bytes.NewReader(content)); err != nil {
   551  		return ocispec.Descriptor{}, err
   552  	}
   553  	return desc, nil
   554  }
   555  
   556  // singleResolver implements Resolver by always returning R,
   557  // and mapping module paths directly to repository paths in
   558  // the registry.
   559  type singleResolver struct {
   560  	R ociregistry.Interface
   561  }
   562  
   563  func (r singleResolver) ResolveToRegistry(mpath, vers string) (RegistryLocation, error) {
   564  	return RegistryLocation{
   565  		Registry:   r.R,
   566  		Repository: mpath,
   567  		Tag:        vers,
   568  	}, nil
   569  }
   570  
   571  // manifestRefs returns an iterator that produces all the references
   572  // contained in m.
   573  func manifestRefs(m ocispec.Manifest) func(func(ociregistry.Descriptor) bool) {
   574  	return func(yield func(ociregistry.Descriptor) bool) {
   575  		if !yield(m.Config) {
   576  			return
   577  		}
   578  		for _, desc := range m.Layers {
   579  			if !yield(desc) {
   580  				return
   581  			}
   582  		}
   583  		// For completeness, although we shouldn't actually use this
   584  		// logic.
   585  		if m.Subject != nil {
   586  			yield(*m.Subject)
   587  		}
   588  	}
   589  }