From 2d36ec7c843a2d88736569c701809cf46a08548d Mon Sep 17 00:00:00 2001 From: Edward McFarlane Date: Tue, 29 Oct 2024 13:38:21 -0400 Subject: [PATCH] Add bufplugin pkg --- private/bufpkg/bufmodule/digest.go | 1 + private/bufpkg/bufmodule/module_ref.go | 2 +- private/bufpkg/bufparse/bufparse.go | 1 + private/bufpkg/bufparse/errors.go | 5 + private/bufpkg/bufplugin/bufplugin.go | 16 ++ private/bufpkg/bufplugin/commit.go | 72 ++++++ private/bufpkg/bufplugin/digest.go | 211 ++++++++++++++++++ private/bufpkg/bufplugin/errors.go | 69 ++++++ private/bufpkg/bufplugin/plugin_data.go | 119 ++++++++++ .../bufpkg/bufplugin/plugin_data_provider.go | 57 +++++ private/bufpkg/bufplugin/plugin_full_name.go | 124 ++++++++++ private/bufpkg/bufplugin/plugin_key.go | 116 ++++++++++ .../bufpkg/bufplugin/plugin_key_provider.go | 52 +++++ private/bufpkg/bufplugin/plugin_ref.go | 103 +++++++++ private/bufpkg/bufplugin/usage.gen.go | 19 ++ 15 files changed, 966 insertions(+), 1 deletion(-) create mode 100644 private/bufpkg/bufplugin/bufplugin.go create mode 100644 private/bufpkg/bufplugin/commit.go create mode 100644 private/bufpkg/bufplugin/digest.go create mode 100644 private/bufpkg/bufplugin/errors.go create mode 100644 private/bufpkg/bufplugin/plugin_data.go create mode 100644 private/bufpkg/bufplugin/plugin_data_provider.go create mode 100644 private/bufpkg/bufplugin/plugin_full_name.go create mode 100644 private/bufpkg/bufplugin/plugin_key.go create mode 100644 private/bufpkg/bufplugin/plugin_key_provider.go create mode 100644 private/bufpkg/bufplugin/plugin_ref.go create mode 100644 private/bufpkg/bufplugin/usage.gen.go diff --git a/private/bufpkg/bufmodule/digest.go b/private/bufpkg/bufmodule/digest.go index cab628cafa..ad4b788857 100644 --- a/private/bufpkg/bufmodule/digest.go +++ b/private/bufpkg/bufmodule/digest.go @@ -97,6 +97,7 @@ type Digest interface { fmt.Stringer // Type returns the type of digest. + // // Always a valid value. Type() DigestType // Value returns the digest value. diff --git a/private/bufpkg/bufmodule/module_ref.go b/private/bufpkg/bufmodule/module_ref.go index c1938eb005..de8f6e12c2 100644 --- a/private/bufpkg/bufmodule/module_ref.go +++ b/private/bufpkg/bufmodule/module_ref.go @@ -61,7 +61,7 @@ func ParseModuleRef(moduleRefString string) (ModuleRef, error) { if err != nil { return nil, err } - // We don't rely on constructors for ParseErrors. + // We don't rely on constructors for bufparse.ParseErrors. return NewModuleRef(registry, owner, name, ref) } diff --git a/private/bufpkg/bufparse/bufparse.go b/private/bufpkg/bufparse/bufparse.go index bd0d7468b5..572e195a84 100644 --- a/private/bufpkg/bufparse/bufparse.go +++ b/private/bufpkg/bufparse/bufparse.go @@ -12,4 +12,5 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package bufparse parses full names and references. package bufparse diff --git a/private/bufpkg/bufparse/errors.go b/private/bufpkg/bufparse/errors.go index 2058afbd07..e1c09fad0b 100644 --- a/private/bufpkg/bufparse/errors.go +++ b/private/bufpkg/bufparse/errors.go @@ -37,6 +37,11 @@ type ParseError struct { } // NewParseError returns a new ParseError. +// +// The typeString is the user-consumable string representing of the type that was attempted to be parsed. +// The input is the input string that was attempted to be parsed. +// The err is the underlying error. The error may be a *ParseError itself. This +// is an error we may give back to the user, use pretty strings that should be read. func NewParseError(typeString string, input string, err error) *ParseError { return &ParseError{ typeString: typeString, diff --git a/private/bufpkg/bufplugin/bufplugin.go b/private/bufpkg/bufplugin/bufplugin.go new file mode 100644 index 0000000000..3e18be9213 --- /dev/null +++ b/private/bufpkg/bufplugin/bufplugin.go @@ -0,0 +1,16 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package bufplugin contains the core primitives for working with Buf plugins. +package bufplugin diff --git a/private/bufpkg/bufplugin/commit.go b/private/bufpkg/bufplugin/commit.go new file mode 100644 index 0000000000..412a6721f0 --- /dev/null +++ b/private/bufpkg/bufplugin/commit.go @@ -0,0 +1,72 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bufplugin + +import ( + "sync" + "time" +) + +// Commit represents a Commit for a Plugin on the BSR. +type Commit interface { + // PluginKey returns the PluginKey for the Commit. + PluginKey() PluginKey + // CreateTime returns the time the Commit was created on the BSR. + CreateTime() (time.Time, error) + + isCommit() +} + +// NewCommit returns a new Commit. +func NewCommit( + pluginKey PluginKey, + getCreateTime func() (time.Time, error), +) Commit { + return newCommit( + pluginKey, + getCreateTime, + ) +} + +// *** PRIVATE *** + +type commit struct { + pluginKey PluginKey + getCreateTime func() (time.Time, error) +} + +func newCommit( + pluginKey PluginKey, + getCreateTime func() (time.Time, error), +) *commit { + return &commit{ + pluginKey: pluginKey, + getCreateTime: sync.OnceValues(getCreateTime), + } +} + +func (c *commit) PluginKey() PluginKey { + return c.pluginKey +} + +func (c *commit) CreateTime() (time.Time, error) { + // This may invoke tamper-proofing per newCommit construction. + if _, err := c.pluginKey.Digest(); err != nil { + return time.Time{}, err + } + return c.getCreateTime() +} + +func (*commit) isCommit() {} diff --git a/private/bufpkg/bufplugin/digest.go b/private/bufpkg/bufplugin/digest.go new file mode 100644 index 0000000000..d4006662b3 --- /dev/null +++ b/private/bufpkg/bufplugin/digest.go @@ -0,0 +1,211 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bufplugin + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/bufbuild/buf/private/bufpkg/bufcas" + "github.com/bufbuild/buf/private/bufpkg/bufparse" + "github.com/bufbuild/buf/private/pkg/syserror" +) + +const ( + // DigestTypeP1 represents the p1 plugin digest type. + // + // The string value of this is "p1". + DigestTypeP1 = iota + 1 +) + +var ( + // AllDigestTypes are all known DigestTypes. + AllDigestTypes = []DigestType{ + DigestTypeP1, + } + digestTypeToString = map[DigestType]string{ + DigestTypeP1: "p1", + } + stringToDigestType = map[string]DigestType{ + "p1": DigestTypeP1, + } +) + +// DigestType is a type of digest. +type DigestType int + +// ParseDigestType parses a DigestType from its string representation. +// +// This reverses DigestType.String(). +// +// Returns an error of type *bufparse.ParseError if thie string could not be parsed. +func ParseDigestType(s string) (DigestType, error) { + d, ok := stringToDigestType[s] + if !ok { + return 0, bufparse.NewParseError( + "plugin digest type", + s, + fmt.Errorf("unknown type: %q", s), + ) + } + return d, nil +} + +// String prints the string representation of the DigestType. +func (d DigestType) String() string { + s, ok := digestTypeToString[d] + if !ok { + return strconv.Itoa(int(d)) + } + return s +} + +// Digest is a digest of some content. +// +// It consists of a DigestType and a digest value. +type Digest interface { + // String() prints typeString:hexValue. + fmt.Stringer + + // Type returns the type of digest. + // + // Always a valid value. + Type() DigestType + // Value returns the digest value. + // + // Always non-empty. + Value() []byte + + isDigest() +} + +// NewDigest creates a new Digest. +func NewDigest(digestType DigestType, bufcasDigest bufcas.Digest) (Digest, error) { + switch digestType { + case DigestTypeP1: + if bufcasDigest.Type() != bufcas.DigestTypeShake256 { + return nil, syserror.Newf( + "trying to create a %v Digest for a cas Digest of type %v", + digestType, + bufcasDigest.Type(), + ) + } + return newDigest(digestType, bufcasDigest), nil + default: + // This is a system error. + return nil, syserror.Newf("unknown DigestType: %v", digestType) + } +} + +// ParseDigest parses a Digest from its string representation. +// +// A Digest string is of the form typeString:hexValue. +// The string is expected to be non-empty, If not, an error is treutned. +// +// This reverses Digest.String(). +func ParseDigest(s string) (Digest, error) { + if s == "" { + // This should be considered a system error. + return nil, errors.New("empty string passed to ParseDigest") + } + digestTypeString, hexValue, ok := strings.Cut(s, ":") + if !ok { + return nil, bufparse.NewParseError( + "plugin digest", s, + errors.New(`must be in the form "digest_type:digest_hex_value"`), + ) + } + digestType, err := ParseDigestType(digestTypeString) + if err != nil { + return nil, bufparse.NewParseError( + "plugin digest", + digestTypeString, + err, + ) + } + value, err := hex.DecodeString(hexValue) + if err != nil { + return nil, bufparse.NewParseError( + "plugin digest", + s, + errors.New(`could not parse hex: must in the form "digest_type:digest_hex_value"`), + ) + } + switch digestType { + case DigestTypeP1: + bufcasDigest, err := bufcas.NewDigest(value) + if err != nil { + return nil, err + } + return NewDigest(digestType, bufcasDigest) + default: + return nil, syserror.Newf("unknown DigestType: %v", digestType) + } +} + +// DigestEqual returns true if the given Digests are considered equal. +// +// If both Digests are nil, this returns true. +// +// This checks both the DigestType and Digest value. +func DigestEqual(a Digest, b Digest) bool { + if (a == nil) != (b == nil) { + return false + } + if a == nil { + return true + } + if a.Type() != b.Type() { + return false + } + return bytes.Equal(a.Value(), b.Value()) +} + +/// *** PRIVATE *** + +type digest struct { + digestType DigestType + bufcasDigest bufcas.Digest + // Cache as we call String pretty often. + // We could do this lazily but not worth it. + stringValue string +} + +// validation should occur outside of this function. +func newDigest(digestType DigestType, bufcasDigest bufcas.Digest) *digest { + return &digest{ + digestType: digestType, + bufcasDigest: bufcasDigest, + stringValue: digestType.String() + ":" + hex.EncodeToString(bufcasDigest.Value()), + } +} + +func (d *digest) Type() DigestType { + return d.digestType +} + +func (d *digest) Value() []byte { + return d.bufcasDigest.Value() +} + +func (d *digest) String() string { + return d.stringValue +} + +func (*digest) isDigest() {} diff --git a/private/bufpkg/bufplugin/errors.go b/private/bufpkg/bufplugin/errors.go new file mode 100644 index 0000000000..8543df2fd0 --- /dev/null +++ b/private/bufpkg/bufplugin/errors.go @@ -0,0 +1,69 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bufplugin + +import ( + "strings" + + "github.com/bufbuild/buf/private/pkg/uuidutil" + "github.com/google/uuid" +) + +// DigestMismatchError is the error returned if the Digest of a downloaded Module or Commit +// does not match the expected digest in a buf.lock file. +type DigestMismatchError struct { + PluginFullName PluginFullName + CommitID uuid.UUID + ExpectedDigest Digest + ActualDigest Digest +} + +// Error implements the error interface. +func (m *DigestMismatchError) Error() string { + if m == nil { + return "" + } + var builder strings.Builder + _, _ = builder.WriteString(`*** Digest verification failed`) + if m.PluginFullName != nil { + _, _ = builder.WriteString(` for "`) + _, _ = builder.WriteString(m.PluginFullName.String()) + if m.CommitID != uuid.Nil { + _, _ = builder.WriteString(`:`) + _, _ = builder.WriteString(uuidutil.ToDashless(m.CommitID)) + } + _, _ = builder.WriteString(`"`) + } + _, _ = builder.WriteString(` ***`) + _, _ = builder.WriteString("\n") + if m.ExpectedDigest != nil && m.ActualDigest != nil { + _, _ = builder.WriteString("\t") + _, _ = builder.WriteString(`Expected digest (from buf.lock): "`) + _, _ = builder.WriteString(m.ExpectedDigest.String()) + _, _ = builder.WriteString(`"`) + _, _ = builder.WriteString("\n") + _, _ = builder.WriteString("\t") + _, _ = builder.WriteString(`Actual digest: "`) + _, _ = builder.WriteString(m.ActualDigest.String()) + _, _ = builder.WriteString(`"`) + _, _ = builder.WriteString("\n") + } + _, _ = builder.WriteString("\t") + _, _ = builder.WriteString(`This may be the result of a hand-edited or corrupted buf.lock file, a corrupted local cache, and/or an attack.`) + _, _ = builder.WriteString("\n") + _, _ = builder.WriteString("\t") + _, _ = builder.WriteString(`To clear your local cache, run "buf registry cc".`) + return builder.String() +} diff --git a/private/bufpkg/bufplugin/plugin_data.go b/private/bufpkg/bufplugin/plugin_data.go new file mode 100644 index 0000000000..7ab0216e0c --- /dev/null +++ b/private/bufpkg/bufplugin/plugin_data.go @@ -0,0 +1,119 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bufplugin + +import ( + "bytes" + "context" + "sync" + + "github.com/bufbuild/buf/private/bufpkg/bufcas" +) + +// PluginData presents the raw bytes for a plugin. +// +// A PluginData generally represents the data for a single Plugin read from the +// BSR API or a cache. +// +// Tamper-proofing is done as part of every function. +type PluginData interface { + // PluginKey used to downoad this PluginData. + // + // The Digest from this PluginKey is used for tamper-proofing. It will be checked + // against the actual data downloaded before Data() returns. + PluginKey() PluginKey + // Data returns the bytes of the Plugin. + // + // This is the raw bytes of the Wasm module. + Data() ([]byte, error) + + isPluginData() +} + +// NewPluginData returns a new PluginData. +func NewPluginData( + ctx context.Context, + pluginKey PluginKey, + getData func() ([]byte, error), +) (PluginData, error) { + return newPluginData( + ctx, + pluginKey, + getData, + ) +} + +// *** PRIVATE *** + +type pluginData struct { + pluginKey PluginKey + getData func() ([]byte, error) + + checkDigest func() error +} + +func newPluginData( + ctx context.Context, + pluginKey PluginKey, + getData func() ([]byte, error), +) (*pluginData, error) { + pluginData := &pluginData{ + pluginKey: pluginKey, + getData: getData, + } + pluginData.checkDigest = sync.OnceValue(func() error { + pluginData, err := pluginData.getData() + if err != nil { + return err + } + bufcasDigest, err := bufcas.NewDigestForContent( + bytes.NewReader(pluginData), + ) + if err != nil { + return err + } + actualDigest, err := NewDigest(DigestTypeP1, bufcasDigest) + if err != nil { + return err + } + expectedDigest, err := pluginKey.Digest() + if err != nil { + return err + } + if !DigestEqual(actualDigest, expectedDigest) { + return &DigestMismatchError{ + PluginFullName: pluginKey.PluginFullName(), + CommitID: pluginKey.CommitID(), + ExpectedDigest: expectedDigest, + ActualDigest: actualDigest, + } + } + return nil + }) + return pluginData, nil +} + +func (p *pluginData) PluginKey() PluginKey { + return p.pluginKey +} + +func (p *pluginData) Data() ([]byte, error) { + if err := p.checkDigest(); err != nil { + return nil, err + } + return p.getData() +} + +func (*pluginData) isPluginData() {} diff --git a/private/bufpkg/bufplugin/plugin_data_provider.go b/private/bufpkg/bufplugin/plugin_data_provider.go new file mode 100644 index 0000000000..a12b50a2a3 --- /dev/null +++ b/private/bufpkg/bufplugin/plugin_data_provider.go @@ -0,0 +1,57 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bufplugin + +import ( + "context" + "io/fs" +) + +var ( + // NopPluginDataProvider is a no-op PluginDataProvider. + NopPluginDataProvider PluginDataProvider = nopPluginDataProvider{} +) + +// PluginDataProvider provides PluginsDatas. +type PluginDataProvider interface { + // GetPluginDatasForPluginKeys gets the PluginDatas for the PluginKeys. + // + // Returned PluginDatas will be in the same order as the input PluginKeys. + // + // The input PluginKeys are expected to be unique by PluginFullName. The implementation + // may error if this is not the case. + // + // The input PluginKeys are expected to have the same DigestType. The implementation + // may error if this is not the case. + // + // If there is no error, the length of the PluginDatas returned will match the length of the PluginKeys. + // If there is an error, no PluginDatas will be returned. + // If any PluginKey is not found, an error with fs.ErrNotExist will be returned. + GetPluginDatasForPluginKeys( + context.Context, + []PluginKey, + ) ([]PluginData, error) +} + +// *** PRIVATE *** + +type nopPluginDataProvider struct{} + +func (nopPluginDataProvider) GetPluginDatasForPluginKeys( + context.Context, + []PluginKey, +) ([]PluginData, error) { + return nil, fs.ErrNotExist +} diff --git a/private/bufpkg/bufplugin/plugin_full_name.go b/private/bufpkg/bufplugin/plugin_full_name.go new file mode 100644 index 0000000000..4b078fafff --- /dev/null +++ b/private/bufpkg/bufplugin/plugin_full_name.go @@ -0,0 +1,124 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bufplugin + +import ( + "errors" + "fmt" + "strings" + + "github.com/bufbuild/buf/private/bufpkg/bufparse" + "github.com/bufbuild/buf/private/pkg/netext" +) + +// PluginFullName represents the full name of the Plugin, including its registry, owner, and name. +type PluginFullName interface { + // String returns "registry/owner/name". + fmt.Stringer + + // Registry returns the hostname of the BSR instance that this Plugin is contained within. + Registry() string + // Owner returns the name of the user or organization that owns this Plugin. + Owner() string + // Name returns the name of the Plugin. + Name() string + + isPluginFullName() +} + +// NewPluginFullName returns a new PluginFullName for the given components. +func NewPluginFullName( + registry string, + owner string, + name string, +) (PluginFullName, error) { + return newPluginFullName(registry, owner, name) +} + +// ParsePluginFullName parses a PluginFullName from a string in the form "registry/owner/name". +func ParsePluginFullName(pluginFullNameString string) (PluginFullName, error) { + registry, owner, name, err := bufparse.ParseFullNameComponents(pluginFullNameString) + if err != nil { + return nil, err + } + return newPluginFullName(registry, owner, name) +} + +// *** PRIVATE *** + +type pluginFullName struct { + registry string + owner string + name string +} + +func newPluginFullName( + registry string, + owner string, + name string, +) (*pluginFullName, error) { + if err := validateModuleFullNameParameters(registry, owner, name); err != nil { + return nil, err + } + return &pluginFullName{ + registry: registry, + owner: owner, + name: name, + }, nil +} + +func (p *pluginFullName) Registry() string { + return p.registry +} + +func (p *pluginFullName) Owner() string { + return p.owner +} + +func (p *pluginFullName) Name() string { + return p.name +} + +func (p *pluginFullName) String() string { + return p.registry + "/" + p.owner + "/" + p.name +} + +func (*pluginFullName) isPluginFullName() {} + +func validateModuleFullNameParameters( + registry string, + owner string, + name string, +) error { + if registry == "" { + return errors.New("registry is empty") + } + if _, err := netext.ValidateHostname(registry); err != nil { + return fmt.Errorf("registry %q is not a valid hostname: %w", registry, err) + } + if owner == "" { + return errors.New("owner is empty") + } + if strings.Contains(owner, "/") { + return fmt.Errorf("owner %q cannot contain slashes", owner) + } + if name == "" { + return errors.New("name is empty") + } + if strings.Contains(name, "/") { + return fmt.Errorf("name %q cannot contain slashes", name) + } + return nil +} diff --git a/private/bufpkg/bufplugin/plugin_key.go b/private/bufpkg/bufplugin/plugin_key.go new file mode 100644 index 0000000000..92994d6c65 --- /dev/null +++ b/private/bufpkg/bufplugin/plugin_key.go @@ -0,0 +1,116 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bufplugin + +import ( + "errors" + "fmt" + "sync" + + "github.com/bufbuild/buf/private/pkg/uuidutil" + "github.com/google/uuid" +) + +// PluginKey provides identifying information for a Plugin. +// +// PluginKeys are returned from PluginKeyProviders, and represent a Plugin's complete +// identity. They also match to what we store in buf.lock files. PluginKeys can be used +// to get Plugins via a PluginProvider. +type PluginKey interface { + // String returns "registry/owner/name:dashlessCommitID". + fmt.Stringer + + // PluginFullName returns the full name of the Plugin. + // + // Always present. + PluginFullName() PluginFullName + // CommitID returns the ID of the Commit. + // + // It is up to the caller to convert this to a dashless ID when necessary. + // + // Always present, that is CommitID() == uuid.Nil will always be false. + CommitID() uuid.UUID + // Digest returns the Plugin digest. + // + // Note this is *not* a bufcas.Digest - this is a Digest. + // bufcas.Digests are a lower-level type that just deal in terms of + // files and content. A PluginDigest is a specific algorithm applied to + // the plugin data. + Digest() (Digest, error) + + isPluginKey() +} + +// NewPluginKey returns a new PluginKey. +// +// The Digest will be loaded lazily if needed. Note this means that NewPluginKey does +// *not* validate the digest. If you need to validate the digset, call Digest() and evaluate +// the returned error. +func NewPluginKey( + pluginFullName PluginFullName, + commitID uuid.UUID, + getDigest func() (Digest, error), +) (PluginKey, error) { + return newPluginKey( + pluginFullName, + commitID, + getDigest, + ) +} + +// ** PRIVATE ** + +type pluginKey struct { + pluginFullName PluginFullName + commitID uuid.UUID + + getDigest func() (Digest, error) +} + +func newPluginKey( + pluginFullName PluginFullName, + commitID uuid.UUID, + getDigest func() (Digest, error), +) (*pluginKey, error) { + if pluginFullName == nil { + return nil, errors.New("nil PluginFullName when constructing PluginKey") + } + if commitID == uuid.Nil { + return nil, errors.New("empty commitID when constructing PluginKey") + } + return &pluginKey{ + pluginFullName: pluginFullName, + commitID: commitID, + getDigest: sync.OnceValues(getDigest), + }, nil +} + +func (p *pluginKey) PluginFullName() PluginFullName { + return p.pluginFullName +} + +func (p *pluginKey) CommitID() uuid.UUID { + return p.commitID +} + +func (p *pluginKey) Digest() (Digest, error) { + return p.getDigest() +} + +func (p *pluginKey) String() string { + return p.pluginFullName.String() + ":" + uuidutil.ToDashless(p.commitID) +} + +func (*pluginKey) isPluginKey() {} diff --git a/private/bufpkg/bufplugin/plugin_key_provider.go b/private/bufpkg/bufplugin/plugin_key_provider.go new file mode 100644 index 0000000000..57e1050471 --- /dev/null +++ b/private/bufpkg/bufplugin/plugin_key_provider.go @@ -0,0 +1,52 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bufplugin + +import ( + "context" + "io/fs" +) + +var ( + // NopPluginKeyProvider is a no-op PluginKeyProvider. + NopPluginKeyProvider PluginKeyProvider = nopPluginKeyProvider{} +) + +// PluginKeyProvider provides PluginKeys for PluginRefs. +type PluginKeyProvider interface { + // GetPluginKeysForPluginRefs gets the PluginKets for the given PluginRefs. + // + // Returned PluginKeys will be in the same order as the input PluginRefs. + // + // The input PluginRefs are expected to be unique by PluginFullName. The implementation + // may error if this is not the case. + // + // If there is no error, the length of the PluginKeys returned will match the length of the PluginRefs. + // If there is an error, no PluginKeys will be returned. + // If any PluginRef is not found, an error with fs.ErrNotExist will be returned. + GetPluginKeysForPluginRefs(context.Context, []PluginRef, DigestType) ([]PluginKey, error) +} + +// *** PRIVATE *** + +type nopPluginKeyProvider struct{} + +func (nopPluginKeyProvider) GetPluginKeysForPluginRefs( + context.Context, + []PluginRef, + DigestType, +) ([]PluginKey, error) { + return nil, fs.ErrNotExist +} diff --git a/private/bufpkg/bufplugin/plugin_ref.go b/private/bufpkg/bufplugin/plugin_ref.go new file mode 100644 index 0000000000..041278923e --- /dev/null +++ b/private/bufpkg/bufplugin/plugin_ref.go @@ -0,0 +1,103 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bufplugin + +import ( + "errors" + "fmt" + + "github.com/bufbuild/buf/private/bufpkg/bufparse" +) + +// PluginRef is an unresolved reference to a Plugin. +type PluginRef interface { + // String returns "registry/owner/name[:ref]". + fmt.Stringer + + // PluginFullName returns the full name of the Plugin. + // + // Always present. + PluginFullName() PluginFullName + // Ref returns the reference within the Plugin. + // + // May be a label or dashless commitID. + // + // May be empty, in which case this references the commit of the default label of the Plugin. + Ref() string + + isPluginRef() +} + +// NewPluginRef returns a new PluginRef for the given compoonents. +func NewPluginRef( + registry string, + owner string, + name string, + ref string, +) (PluginRef, error) { + pluginFullName, err := NewPluginFullName(registry, owner, name) + if err != nil { + return nil, err + } + return newPluginRef(pluginFullName, ref) +} + +// ParsePluginRef parses a PluginRef from a string in the form "registry/owner/name[:ref]". +func ParsePluginRef(pluginRefString string) (PluginRef, error) { + // Returns *bufparse.ParseErrors. + registry, owner, name, ref, err := bufparse.ParseRefComponents(pluginRefString) + if err != nil { + return nil, err + } + // We don't rely on constructors for bufparse.ParseErrors. + return NewPluginRef(registry, owner, name, ref) +} + +// *** PRIVATE *** + +type pluginRef struct { + pluginFullName PluginFullName + ref string +} + +func newPluginRef( + pluginFullName PluginFullName, + ref string, +) (*pluginRef, error) { + if pluginFullName == nil { + return nil, errors.New("nil PluginFullName when constructing PluginRef") + } + return &pluginRef{ + pluginFullName: pluginFullName, + ref: ref, + }, nil +} + +func (m *pluginRef) PluginFullName() PluginFullName { + return m.pluginFullName +} + +func (m *pluginRef) Ref() string { + return m.ref +} + +func (m *pluginRef) String() string { + if m.ref == "" { + return m.pluginFullName.String() + } + return m.pluginFullName.String() + ":" + m.ref +} + +func (*pluginRef) isPluginRef() {} diff --git a/private/bufpkg/bufplugin/usage.gen.go b/private/bufpkg/bufplugin/usage.gen.go new file mode 100644 index 0000000000..6ffd8bf974 --- /dev/null +++ b/private/bufpkg/bufplugin/usage.gen.go @@ -0,0 +1,19 @@ +// Copyright 2020-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Generated. DO NOT EDIT. + +package bufplugin + +import _ "github.com/bufbuild/buf/private/usage"